完善 web 端
This commit is contained in:
parent
b473a110a0
commit
c03466a34b
2
web/.env.dev
Normal file
2
web/.env.dev
Normal file
@ -0,0 +1,2 @@
|
||||
NODE_ENV=development
|
||||
VUE_APP_SERVER=http://localhost:8000
|
2
web/.env.prod
Normal file
2
web/.env.prod
Normal file
@ -0,0 +1,2 @@
|
||||
NODE_ENV=production
|
||||
VUE_APP_SERVER=http://train.imooc.com
|
@ -1,29 +1,24 @@
|
||||
# web
|
||||
|
||||
## Project setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
|
3480
web/package-lock.json
generated
3480
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,15 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"web-dev": "vue-cli-service serve --mode dev --port 9000",
|
||||
"web-prod": "vue-cli-service serve --mode prod --port 9000",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"ant-design-vue": "^3.2.15",
|
||||
"axios": "^1.3.2",
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
@ -36,7 +40,10 @@
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": 0,
|
||||
"no-undef": 0
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
@ -5,6 +5,8 @@
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
||||
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
|
||||
<script src="<%= BASE_URL %>js/session-storage.js"></script>
|
||||
<script src="<%= BASE_URL %>js/tool.js"></script>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
21
web/public/js/session-storage.js
Normal file
21
web/public/js/session-storage.js
Normal file
@ -0,0 +1,21 @@
|
||||
// 所有的session key都在这里统一定义,可以避免多个功能使用同一个key
|
||||
SESSION_ORDER = "SESSION_ORDER";
|
||||
SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";
|
||||
|
||||
SessionStorage = {
|
||||
get: function (key) {
|
||||
var v = sessionStorage.getItem(key);
|
||||
if (v && typeof (v) !== "undefined" && v !== "undefined") {
|
||||
return JSON.parse(v);
|
||||
}
|
||||
},
|
||||
set: function (key, data) {
|
||||
sessionStorage.setItem(key, JSON.stringify(data));
|
||||
},
|
||||
remove: function (key) {
|
||||
sessionStorage.removeItem(key);
|
||||
},
|
||||
clearAll: function () {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
};
|
73
web/public/js/tool.js
Normal file
73
web/public/js/tool.js
Normal file
@ -0,0 +1,73 @@
|
||||
Tool = {
|
||||
/**
|
||||
* 空校验 null或""都返回true
|
||||
*/
|
||||
isEmpty: (obj) => {
|
||||
if ((typeof obj === 'string')) {
|
||||
return !obj || obj.replace(/\s+/g, "") === ""
|
||||
} else {
|
||||
return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 非空校验
|
||||
*/
|
||||
isNotEmpty: (obj) => {
|
||||
return !Tool.isEmpty(obj);
|
||||
},
|
||||
|
||||
/**
|
||||
* 对象复制
|
||||
* @param obj
|
||||
*/
|
||||
copy: (obj) => {
|
||||
if (Tool.isNotEmpty(obj)) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 使用递归将数组转为树形结构
|
||||
* 父ID属性为parent
|
||||
*/
|
||||
array2Tree: (array, parentId) => {
|
||||
if (Tool.isEmpty(array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const c = array[i];
|
||||
// console.log(Number(c.parent), Number(parentId));
|
||||
if (Number(c.parent) === Number(parentId)) {
|
||||
result.push(c);
|
||||
|
||||
// 递归查看当前节点对应的子节点
|
||||
const children = Tool.array2Tree(array, c.id);
|
||||
if (Tool.isNotEmpty(children)) {
|
||||
c.children = children;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成[len]长度的[radix]进制数
|
||||
* @param len
|
||||
* @param radix 默认62
|
||||
* @returns {string}
|
||||
*/
|
||||
uuid: (len, radix = 62) => {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
||||
const uuid = [];
|
||||
radix = radix || chars.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
uuid[i] = chars[0 | Math.random() * radix];
|
||||
}
|
||||
|
||||
return uuid.join('');
|
||||
}
|
||||
};
|
@ -1,31 +1,3 @@
|
||||
<template>
|
||||
<nav>
|
||||
<router-link to="/">Home</router-link>
|
||||
|
|
||||
<router-link to="/about">About</router-link>
|
||||
</nav>
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
|
48
web/src/assets/js/enums.js
Normal file
48
web/src/assets/js/enums.js
Normal file
@ -0,0 +1,48 @@
|
||||
PASSENGER_TYPE = {
|
||||
ADULT: {code: "1", desc: "成人"},
|
||||
CHILD: {code: "2", desc: "儿童"},
|
||||
STUDENT: {code: "3", desc: "学生"}
|
||||
};
|
||||
TRAIN_TYPE = {
|
||||
G: {code: "G", desc: "高铁", priceRate: "1.2"},
|
||||
D: {code: "D", desc: "动车", priceRate: "1"},
|
||||
K: {code: "K", desc: "快速", priceRate: "0.8"}
|
||||
};
|
||||
SEAT_TYPE = {
|
||||
YDZ: {code: "1", desc: "一等座", price: "0.4"},
|
||||
EDZ: {code: "2", desc: "二等座", price: "0.3"},
|
||||
RW: {code: "3", desc: "软卧", price: "0.6"},
|
||||
YW: {code: "4", desc: "硬卧", price: "0.5"}
|
||||
};
|
||||
SEAT_COL = {
|
||||
YDZ_A: {code: "A", desc: "A", type: "1"},
|
||||
YDZ_C: {code: "C", desc: "C", type: "1"},
|
||||
YDZ_D: {code: "D", desc: "D", type: "1"},
|
||||
YDZ_F: {code: "F", desc: "F", type: "1"},
|
||||
EDZ_A: {code: "A", desc: "A", type: "2"},
|
||||
EDZ_B: {code: "B", desc: "B", type: "2"},
|
||||
EDZ_C: {code: "C", desc: "C", type: "2"},
|
||||
EDZ_D: {code: "D", desc: "D", type: "2"},
|
||||
EDZ_F: {code: "F", desc: "F", type: "2"}
|
||||
};
|
||||
|
||||
PASSENGER_TYPE_ARRAY = [{code: "1", desc: "成人"}, {code: "2", desc: "儿童"}, {code: "3", desc: "学生"}];
|
||||
TRAIN_TYPE_ARRAY = [{code: "G", desc: "高铁", priceRate: "1.2"}, {code: "D", desc: "动车", priceRate: "1"}, {
|
||||
code: "K",
|
||||
desc: "快速",
|
||||
priceRate: "0.8"
|
||||
}];
|
||||
SEAT_TYPE_ARRAY = [{code: "1", desc: "一等座", price: "0.4"}, {code: "2", desc: "二等座", price: "0.3"}, {
|
||||
code: "3",
|
||||
desc: "软卧",
|
||||
price: "0.6"
|
||||
}, {code: "4", desc: "硬卧", price: "0.5"}];
|
||||
SEAT_COL_ARRAY = [{code: "A", desc: "A", type: "1"}, {code: "C", desc: "C", type: "1"}, {
|
||||
code: "D",
|
||||
desc: "D",
|
||||
type: "1"
|
||||
}, {code: "F", desc: "F", type: "1"}, {code: "A", desc: "A", type: "2"}, {code: "B", desc: "B", type: "2"}, {
|
||||
code: "C",
|
||||
desc: "C",
|
||||
type: "2"
|
||||
}, {code: "D", desc: "D", type: "2"}, {code: "F", desc: "F", type: "2"}];
|
@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" rel="noopener" target="_blank">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" rel="noopener"
|
||||
target="_blank">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" rel="noopener"
|
||||
target="_blank">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" rel="noopener"
|
||||
target="_blank">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" rel="noopener"
|
||||
target="_blank">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" rel="noopener" target="_blank">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" rel="noopener" target="_blank">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" rel="noopener" target="_blank">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" rel="noopener" target="_blank">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" rel="noopener" target="_blank">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" rel="noopener" target="_blank">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" rel="noopener" target="_blank">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" rel="noopener" target="_blank">vue-devtools</a>
|
||||
</li>
|
||||
<li><a href="https://vue-loader.vuejs.org" rel="noopener" target="_blank">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" rel="noopener" target="_blank">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
85
web/src/components/station-select.vue
Normal file
85
web/src/components/station-select.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a-select v-model:value="name" :filterOption="filterNameOption" :style="'width: ' + localWidth"
|
||||
allowClear
|
||||
placeholder="请选择车站" show-search
|
||||
@change="onChange">
|
||||
<a-select-option v-for="item in stations" :key="item.name" :label="item.name + item.namePinyin + item.namePy"
|
||||
:value="item.name">
|
||||
{{ item.name }} {{ item.namePinyin }} ~ {{ item.namePy }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {defineComponent, onMounted, ref, watch} from 'vue';
|
||||
import axios from "axios";
|
||||
import {notification} from "ant-design-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "station-select-view",
|
||||
props: ["modelValue", "width"],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, {emit}) {
|
||||
const name = ref();
|
||||
const stations = ref([]);
|
||||
const localWidth = ref(props.width);
|
||||
if (Tool.isEmpty(props.width)) {
|
||||
localWidth.value = "100%";
|
||||
}
|
||||
|
||||
// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效
|
||||
watch(() => props.modelValue, () => {
|
||||
console.log("props.modelValue", props.modelValue);
|
||||
name.value = props.modelValue;
|
||||
}, {immediate: true});
|
||||
|
||||
/**
|
||||
* 查询所有的车站,用于车站下拉框
|
||||
*/
|
||||
const queryAllStation = () => {
|
||||
axios.get("/business/station/query-all").then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
stations.value = data.content;
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 车站下拉框筛选
|
||||
*/
|
||||
const filterNameOption = (input, option) => {
|
||||
console.log(input, option);
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将当前组件的值响应给父组件
|
||||
* @param value
|
||||
*/
|
||||
const onChange = (value) => {
|
||||
emit('update:modelValue', value);
|
||||
let station = stations.value.filter(item => item.code === value)[0];
|
||||
if (Tool.isEmpty(station)) {
|
||||
station = {};
|
||||
}
|
||||
emit('change', station);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryAllStation();
|
||||
});
|
||||
|
||||
return {
|
||||
name,
|
||||
stations,
|
||||
filterNameOption,
|
||||
onChange,
|
||||
localWidth
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
72
web/src/components/the-header.vue
Normal file
72
web/src/components/the-header.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<div class="logo">
|
||||
<router-link style="color: white; font-size: 18px" to="/welcome">
|
||||
12306
|
||||
</router-link>
|
||||
</div>
|
||||
<div style="float: right; color: white;">
|
||||
您好:{{ member.mobile }}
|
||||
<router-link style="color: white;" to="/login">
|
||||
退出登录
|
||||
</router-link>
|
||||
</div>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:style="{ lineHeight: '64px' }"
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
>
|
||||
<a-menu-item key="/welcome">
|
||||
<router-link to="/welcome">
|
||||
<coffee-outlined/> 欢迎
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/passenger">
|
||||
<router-link to="/passenger">
|
||||
<user-outlined/> 乘车人管理
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/ticket">
|
||||
<router-link to="/ticket">
|
||||
<user-outlined/> 余票查询
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, ref, watch} from 'vue';
|
||||
import store from "@/store";
|
||||
import router from '@/router'
|
||||
|
||||
export default defineComponent({
|
||||
name: "the-header-view",
|
||||
setup() {
|
||||
let member = store.state.member;
|
||||
const selectedKeys = ref([]);
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue) => {
|
||||
console.log('watch', newValue);
|
||||
selectedKeys.value = [];
|
||||
selectedKeys.value.push(newValue);
|
||||
}, {immediate: true});
|
||||
return {
|
||||
member,
|
||||
selectedKeys
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
.logo {
|
||||
float: left;
|
||||
height: 31px;
|
||||
width: 150px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
51
web/src/components/the-sider.vue
Normal file
51
web/src/components/the-sider.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a-layout-sider style="background: #fff" width="200">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:style="{ height: '100%', borderRight: 0 }"
|
||||
mode="inline"
|
||||
>
|
||||
<a-menu-item key="/welcome">
|
||||
<router-link to="/welcome">
|
||||
<coffee-outlined/> 欢迎
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/passenger">
|
||||
<router-link to="/passenger">
|
||||
<user-outlined/> 乘车人管理
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/ticket">
|
||||
<router-link to="/ticket">
|
||||
<user-outlined/> 余票查询
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, ref, watch} from 'vue';
|
||||
import router from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "the-sider-view",
|
||||
setup() {
|
||||
const selectedKeys = ref([]);
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue) => {
|
||||
console.log('watch', newValue);
|
||||
selectedKeys.value = [];
|
||||
selectedKeys.value.push(newValue);
|
||||
}, {immediate: true});
|
||||
return {
|
||||
selectedKeys
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
|
||||
</style>
|
85
web/src/components/train-select.vue
Normal file
85
web/src/components/train-select.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a-select v-model:value="trainCode" :filterOption="filterTrainCodeOption" :style="'width: ' + localWidth"
|
||||
allowClear
|
||||
placeholder="请选择车次" show-search
|
||||
@change="onChange">
|
||||
<a-select-option v-for="item in trains" :key="item.code" :label="item.code + item.start + item.end"
|
||||
:value="item.code">
|
||||
{{ item.code }} {{ item.start }} ~ {{ item.end }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {defineComponent, onMounted, ref, watch} from 'vue';
|
||||
import axios from "axios";
|
||||
import {notification} from "ant-design-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "train-select-view",
|
||||
props: ["modelValue", "width"],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, {emit}) {
|
||||
const trainCode = ref();
|
||||
const trains = ref([]);
|
||||
const localWidth = ref(props.width);
|
||||
if (Tool.isEmpty(props.width)) {
|
||||
localWidth.value = "100%";
|
||||
}
|
||||
|
||||
// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效
|
||||
watch(() => props.modelValue, () => {
|
||||
console.log("props.modelValue", props.modelValue);
|
||||
trainCode.value = props.modelValue;
|
||||
}, {immediate: true});
|
||||
|
||||
/**
|
||||
* 查询所有的车次,用于车次下拉框
|
||||
*/
|
||||
const queryAllTrain = () => {
|
||||
axios.get("/business/train/query-all").then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
trains.value = data.content;
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 车次下拉框筛选
|
||||
*/
|
||||
const filterTrainCodeOption = (input, option) => {
|
||||
console.log(input, option);
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将当前组件的值响应给父组件
|
||||
* @param value
|
||||
*/
|
||||
const onChange = (value) => {
|
||||
emit('update:modelValue', value);
|
||||
let train = trains.value.filter(item => item.code === value)[0];
|
||||
if (Tool.isEmpty(train)) {
|
||||
train = {};
|
||||
}
|
||||
emit('change', train);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryAllTrain();
|
||||
});
|
||||
|
||||
return {
|
||||
trainCode,
|
||||
trains,
|
||||
filterTrainCodeOption,
|
||||
onChange,
|
||||
localWidth
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@ -2,5 +2,52 @@ import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import Antd, {notification} from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/antd.css';
|
||||
import * as Icons from '@ant-design/icons-vue';
|
||||
import axios from 'axios';
|
||||
import './assets/js/enums';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(Antd).use(store).use(router).mount('#app');
|
||||
|
||||
// 全局使用图标
|
||||
const icons = Icons;
|
||||
for (const i in icons) {
|
||||
app.component(i, icons[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* axios拦截器
|
||||
*/
|
||||
axios.interceptors.request.use(function (config) {
|
||||
console.log('请求参数:', config);
|
||||
const _token = store.state.member.token;
|
||||
if (_token) {
|
||||
config.headers.token = _token;
|
||||
console.log("请求headers增加token:", _token);
|
||||
}
|
||||
return config;
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
axios.interceptors.response.use(function (response) {
|
||||
console.log('返回结果:', response);
|
||||
return response;
|
||||
}, error => {
|
||||
console.log('返回错误:', error);
|
||||
const response = error.response;
|
||||
const status = response.status;
|
||||
if (status === 401) {
|
||||
// 判断状态码是401 跳转到登录页
|
||||
console.log("未登录或登录超时,跳到登录页");
|
||||
store.commit("setMember", {});
|
||||
notification.error({description: "未登录或登录超时"});
|
||||
router.push('/login');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
axios.defaults.baseURL = process.env.VUE_APP_SERVER;
|
||||
console.log('环境:', process.env.NODE_ENV);
|
||||
console.log('服务端:', process.env.VUE_APP_SERVER);
|
||||
|
||||
createApp(App).use(store).use(router).mount('#app')
|
||||
|
@ -1,25 +1,58 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import store from "@/store";
|
||||
import {notification} from "ant-design-vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
const routes = [{
|
||||
path: '/login',
|
||||
component: () => import('../views/login.vue')
|
||||
}, {
|
||||
path: '/',
|
||||
component: () => import('../views/main.vue'),
|
||||
meta: {
|
||||
loginRequire: true
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
|
||||
}
|
||||
]
|
||||
children: [{
|
||||
path: 'welcome',
|
||||
component: () => import('../views/main/welcome.vue'),
|
||||
}, {
|
||||
path: 'passenger',
|
||||
component: () => import('../views/main/passenger.vue'),
|
||||
}, {
|
||||
path: 'ticket',
|
||||
component: () => import('../views/main/ticket.vue'),
|
||||
}, {
|
||||
path: 'order',
|
||||
component: () => import('../views/main/order.vue'),
|
||||
}]
|
||||
}, {
|
||||
path: '',
|
||||
redirect: '/welcome'
|
||||
}];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由登录拦截
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 要不要对meta.loginRequire属性做监控拦截
|
||||
if (to.matched.some(function (item) {
|
||||
console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
|
||||
return item.meta.loginRequire
|
||||
})) {
|
||||
const _member = store.state.member;
|
||||
console.log("页面登录校验开始:", _member);
|
||||
if (!_member.token) {
|
||||
console.log("用户未登录或登录超时!");
|
||||
notification.error({description: "未登录或登录超时"});
|
||||
next('/login');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router
|
||||
|
@ -1,9 +1,18 @@
|
||||
import {createStore} from 'vuex'
|
||||
|
||||
const MEMBER = "MEMBER";
|
||||
|
||||
export default createStore({
|
||||
state: {},
|
||||
state: {
|
||||
member: window.SessionStorage.get(MEMBER) || {}
|
||||
},
|
||||
getters: {},
|
||||
mutations: {},
|
||||
mutations: {
|
||||
setMember(state, _member) {
|
||||
state.member = _member;
|
||||
window.SessionStorage.set(MEMBER, _member);
|
||||
}
|
||||
},
|
||||
actions: {},
|
||||
modules: {}
|
||||
})
|
||||
|
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png">
|
||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
109
web/src/views/login.vue
Normal file
109
web/src/views/login.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<a-row class="login">
|
||||
<a-col :offset="8" :span="8" class="login-main">
|
||||
<h1 style="text-align: center">
|
||||
<rocket-two-tone/> 12306售票系统
|
||||
</h1>
|
||||
<a-form
|
||||
:model="loginForm"
|
||||
autocomplete="off"
|
||||
name="basic"
|
||||
>
|
||||
<a-form-item
|
||||
:rules="[{ required: true, message: '请输入手机号!' }]"
|
||||
label=""
|
||||
name="mobile"
|
||||
>
|
||||
<a-input v-model:value="loginForm.mobile" placeholder="手机号"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:rules="[{ required: true, message: '请输入验证码!' }]"
|
||||
label=""
|
||||
name="code"
|
||||
>
|
||||
<a-input v-model:value="loginForm.code">
|
||||
<template #addonAfter>
|
||||
<a @click="sendCode">获取验证码</a>
|
||||
</template>
|
||||
</a-input>
|
||||
<!--<a-input v-model:value="loginForm.code" placeholder="验证码"/>-->
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button block type="primary" @click="login">登录</a-button>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, reactive} from 'vue';
|
||||
import axios from 'axios';
|
||||
import {notification} from 'ant-design-vue';
|
||||
import {useRouter} from 'vue-router'
|
||||
import store from "@/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "login-view",
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
|
||||
const loginForm = reactive({
|
||||
mobile: '13000000000',
|
||||
code: '',
|
||||
});
|
||||
|
||||
const sendCode = () => {
|
||||
axios.post("/member/member/send-code", {
|
||||
mobile: loginForm.mobile
|
||||
}).then(response => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
notification.success({description: '发送验证码成功!'});
|
||||
loginForm.code = "8888";
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const login = () => {
|
||||
axios.post("/member/member/login", loginForm).then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
notification.success({description: '登录成功!'});
|
||||
// 登录成功,跳到控台主页
|
||||
router.push("/welcome");
|
||||
store.commit("setMember", data.content);
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
loginForm,
|
||||
sendCode,
|
||||
login
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.login-main h1 {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.login-main {
|
||||
margin-top: 100px;
|
||||
padding: 30px 30px 20px;
|
||||
border: 2px solid grey;
|
||||
border-radius: 10px;
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
</style>
|
31
web/src/views/main.vue
Normal file
31
web/src/views/main.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<a-layout id="components-layout-demo-top-side-2">
|
||||
<the-header-view></the-header-view>
|
||||
<a-layout>
|
||||
<the-sider-view></the-sider-view>
|
||||
<a-layout-content
|
||||
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
|
||||
>
|
||||
<router-view></router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
<script>
|
||||
import {defineComponent} from 'vue';
|
||||
import TheHeaderView from "@/components/the-header";
|
||||
import TheSiderView from "@/components/the-sider";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TheSiderView,
|
||||
TheHeaderView,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
|
||||
</style>
|
394
web/src/views/main/order.vue
Normal file
394
web/src/views/main/order.vue
Normal file
@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div class="order-train">
|
||||
<span class="order-train-main">{{ dailyTrainTicket.date }}</span>
|
||||
<span class="order-train-main">{{ dailyTrainTicket.trainCode }}</span>次
|
||||
<span class="order-train-main">{{ dailyTrainTicket.start }}</span>站
|
||||
<span class="order-train-main">({{ dailyTrainTicket.startTime }})</span>
|
||||
<span class="order-train-main">——</span>
|
||||
<span class="order-train-main">{{ dailyTrainTicket.end }}</span>站
|
||||
<span class="order-train-main">({{ dailyTrainTicket.endTime }})</span>
|
||||
|
||||
<div class="order-train-ticket">
|
||||
<span v-for="item in seatTypes" :key="item.type">
|
||||
<span>{{ item.desc }}</span>:
|
||||
<span class="order-train-ticket-main">{{ item.price }}¥</span>
|
||||
<span class="order-train-ticket-main">{{ item.count }}</span> 张票
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider></a-divider>
|
||||
<b>勾选要购票的乘客:</b>
|
||||
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions"/>
|
||||
|
||||
<div class="order-tickets">
|
||||
<a-row v-if="tickets.length > 0" class="order-tickets-header">
|
||||
<a-col :span="2">乘客</a-col>
|
||||
<a-col :span="6">身份证</a-col>
|
||||
<a-col :span="4">票种</a-col>
|
||||
<a-col :span="4">座位类型</a-col>
|
||||
</a-row>
|
||||
<a-row v-for="ticket in tickets" :key="ticket.passengerId" class="order-tickets-row">
|
||||
<a-col :span="2">{{ ticket.passengerName }}</a-col>
|
||||
<a-col :span="6">{{ ticket.passengerIdCard }}</a-col>
|
||||
<a-col :span="4">
|
||||
<a-select v-model:value="ticket.passengerType" style="width: 100%">
|
||||
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
|
||||
{{ item.desc }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
|
||||
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
|
||||
{{ item.desc }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div v-if="tickets.length > 0">
|
||||
<a-button size="large" type="primary" @click="finishCheckPassenger">提交订单</a-button>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:visible="visible" cancel-text="取消"
|
||||
ok-text="确认"
|
||||
style="top: 50px; width: 800px" title="请核对以下信息"
|
||||
@ok="handleOk">
|
||||
<div class="order-tickets">
|
||||
<a-row v-if="tickets.length > 0" class="order-tickets-header">
|
||||
<a-col :span="3">乘客</a-col>
|
||||
<a-col :span="15">身份证</a-col>
|
||||
<a-col :span="3">票种</a-col>
|
||||
<a-col :span="3">座位类型</a-col>
|
||||
</a-row>
|
||||
<a-row v-for="ticket in tickets" :key="ticket.passengerId" class="order-tickets-row">
|
||||
<a-col :span="3">{{ ticket.passengerName }}</a-col>
|
||||
<a-col :span="15">{{ ticket.passengerIdCard }}</a-col>
|
||||
<a-col :span="3">
|
||||
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
|
||||
<span v-if="item.code === ticket.passengerType">
|
||||
{{ item.desc }}
|
||||
</span>
|
||||
</span>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<span v-for="item in seatTypes" :key="item.code">
|
||||
<span v-if="item.code === ticket.seatTypeCode">
|
||||
{{ item.desc }}
|
||||
</span>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<br/>
|
||||
<div v-if="chooseSeatType === 0" style="color: red;">
|
||||
您购买的车票不支持选座
|
||||
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
|
||||
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
|
||||
</div>
|
||||
<div v-else style="text-align: center">
|
||||
<a-switch v-for="item in SEAT_COL_ARRAY" :key="item.code" v-model:checked="chooseSeatObj[item.code + '1']"
|
||||
:checked-children="item.desc" :un-checked-children="item.desc"
|
||||
class="choose-seat-item"/>
|
||||
<div v-if="tickets.length > 1">
|
||||
<a-switch v-for="item in SEAT_COL_ARRAY" :key="item.code" v-model:checked="chooseSeatObj[item.code + '2']"
|
||||
:checked-children="item.desc" :un-checked-children="item.desc"
|
||||
class="choose-seat-item"/>
|
||||
</div>
|
||||
<div style="color: #999999">提示:您可以选择{{ tickets.length }}个座位</div>
|
||||
</div>
|
||||
<br/>
|
||||
最终购票:{{ tickets }}
|
||||
最终选座:{{ chooseSeatObj }}
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {computed, defineComponent, onMounted, ref, watch} from 'vue';
|
||||
import axios from "axios";
|
||||
import {notification} from "ant-design-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "order-view",
|
||||
setup() {
|
||||
const passengers = ref([]);
|
||||
const passengerOptions = ref([]);
|
||||
const passengerChecks = ref([]);
|
||||
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
|
||||
console.log("下单的车次信息", dailyTrainTicket);
|
||||
|
||||
const SEAT_TYPE = window.SEAT_TYPE;
|
||||
console.log(SEAT_TYPE)
|
||||
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
|
||||
// {
|
||||
// type: "YDZ",
|
||||
// code: "1",
|
||||
// desc: "一等座",
|
||||
// count: "100",
|
||||
// price: "50",
|
||||
// }
|
||||
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
|
||||
const seatTypes = [];
|
||||
for (let KEY in SEAT_TYPE) {
|
||||
let key = KEY.toLowerCase();
|
||||
if (dailyTrainTicket[key] >= 0) {
|
||||
seatTypes.push({
|
||||
type: KEY,
|
||||
code: SEAT_TYPE[KEY]["code"],
|
||||
desc: SEAT_TYPE[KEY]["desc"],
|
||||
count: dailyTrainTicket[key],
|
||||
price: dailyTrainTicket[key + 'Price'],
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log("本车次提供的座位:", seatTypes)
|
||||
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
|
||||
// {
|
||||
// passengerId: 123,
|
||||
// passengerType: "1",
|
||||
// passengerName: "张三",
|
||||
// passengerIdCard: "12323132132",
|
||||
// seatTypeCode: "1",
|
||||
// seat: "C1"
|
||||
// }
|
||||
const tickets = ref([]);
|
||||
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
|
||||
const visible = ref(false);
|
||||
|
||||
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
|
||||
watch(() => passengerChecks.value, (newVal, oldVal) => {
|
||||
console.log("勾选乘客发生变化", newVal, oldVal)
|
||||
// 每次有变化时,把购票列表清空,重新构造列表
|
||||
tickets.value = [];
|
||||
passengerChecks.value.forEach((item) => tickets.value.push({
|
||||
passengerId: item.id,
|
||||
passengerType: item.type,
|
||||
seatTypeCode: seatTypes[0].code,
|
||||
passengerName: item.name,
|
||||
passengerIdCard: item.idCard
|
||||
}))
|
||||
}, {immediate: true});
|
||||
|
||||
// 0:不支持选座;1:选一等座;2:选二等座
|
||||
const chooseSeatType = ref(0);
|
||||
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
|
||||
const SEAT_COL_ARRAY = computed(() => {
|
||||
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
|
||||
});
|
||||
// 选择的座位
|
||||
// {
|
||||
// A1: false, C1: true,D1: false, F1: false,
|
||||
// A2: false, C2: false,D2: true, F2: false
|
||||
// }
|
||||
const chooseSeatObj = ref({});
|
||||
watch(() => SEAT_COL_ARRAY.value, () => {
|
||||
chooseSeatObj.value = {};
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
SEAT_COL_ARRAY.value.forEach((item) => {
|
||||
chooseSeatObj.value[item.code + i] = false;
|
||||
})
|
||||
}
|
||||
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
|
||||
}, {immediate: true});
|
||||
|
||||
const handleQueryPassenger = () => {
|
||||
axios.get("/member/passenger/query-mine").then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
passengers.value = data.content;
|
||||
passengers.value.forEach((item) => passengerOptions.value.push({
|
||||
label: item.name,
|
||||
value: item
|
||||
}))
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const finishCheckPassenger = () => {
|
||||
console.log("购票列表:", tickets.value);
|
||||
|
||||
if (tickets.value.length > 5) {
|
||||
notification.error({description: '最多只能购买5张车票'});
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
|
||||
// 前端校验不一定准,但前端校验可以减轻后端很多压力
|
||||
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
|
||||
let seatTypesTemp = Tool.copy(seatTypes);
|
||||
for (let i = 0; i < tickets.value.length; i++) {
|
||||
let ticket = tickets.value[i];
|
||||
for (let j = 0; j < seatTypesTemp.length; j++) {
|
||||
let seatType = seatTypesTemp[j];
|
||||
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
|
||||
if (ticket.seatTypeCode === seatType.code) {
|
||||
seatType.count--;
|
||||
if (seatType.count < 0) {
|
||||
notification.error({description: seatType.desc + '余票不足'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("前端余票校验通过");
|
||||
|
||||
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
|
||||
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
|
||||
let ticketSeatTypeCodes = [];
|
||||
for (let i = 0; i < tickets.value.length; i++) {
|
||||
let ticket = tickets.value[i];
|
||||
ticketSeatTypeCodes.push(ticket.seatTypeCode);
|
||||
}
|
||||
// 为购票列表中的所有座位类型去重:[1, 2]
|
||||
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
|
||||
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
|
||||
if (ticketSeatTypeCodesSet.length !== 1) {
|
||||
console.log("选了多种座位,不支持选座");
|
||||
chooseSeatType.value = 0;
|
||||
} else {
|
||||
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
|
||||
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
|
||||
console.log("一等座选座");
|
||||
chooseSeatType.value = SEAT_TYPE.YDZ.code;
|
||||
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
|
||||
console.log("二等座选座");
|
||||
chooseSeatType.value = SEAT_TYPE.EDZ.code;
|
||||
} else {
|
||||
console.log("不是一等座或二等座,不支持选座");
|
||||
chooseSeatType.value = 0;
|
||||
}
|
||||
|
||||
// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
|
||||
if (chooseSeatType.value !== 0) {
|
||||
for (let i = 0; i < seatTypes.length; i++) {
|
||||
let seatType = seatTypes[i];
|
||||
// 找到同类型座位
|
||||
if (ticketSeatTypeCodesSet[0] === seatType.code) {
|
||||
// 判断余票,小于20张就不支持选座
|
||||
if (seatType.count < 20) {
|
||||
console.log("余票小于20张就不支持选座")
|
||||
chooseSeatType.value = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出确认界面
|
||||
visible.value = true;
|
||||
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
console.log("选好的座位:", chooseSeatObj.value);
|
||||
|
||||
// 设置每张票的座位
|
||||
// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
|
||||
for (let i = 0; i < tickets.value.length; i++) {
|
||||
tickets.value[i].seat = null;
|
||||
}
|
||||
let i = -1;
|
||||
// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
|
||||
for (let key in chooseSeatObj.value) {
|
||||
if (chooseSeatObj.value[key]) {
|
||||
i++;
|
||||
if (i > tickets.value.length - 1) {
|
||||
notification.error({description: '所选座位数大于购票数'});
|
||||
return;
|
||||
}
|
||||
tickets.value[i].seat = key;
|
||||
}
|
||||
}
|
||||
if (i > -1 && i < (tickets.value.length - 1)) {
|
||||
notification.error({description: '所选座位数小于购票数'});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("最终购票:", tickets.value);
|
||||
|
||||
axios.post("/business/confirm-order/do", {
|
||||
dailyTrainTicketId: dailyTrainTicket.id,
|
||||
date: dailyTrainTicket.date,
|
||||
trainCode: dailyTrainTicket.trainCode,
|
||||
start: dailyTrainTicket.start,
|
||||
end: dailyTrainTicket.end,
|
||||
tickets: tickets.value
|
||||
}).then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
notification.success({description: "下单成功!"});
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQueryPassenger();
|
||||
});
|
||||
|
||||
return {
|
||||
passengers,
|
||||
dailyTrainTicket,
|
||||
seatTypes,
|
||||
passengerOptions,
|
||||
passengerChecks,
|
||||
tickets,
|
||||
PASSENGER_TYPE_ARRAY,
|
||||
visible,
|
||||
finishCheckPassenger,
|
||||
chooseSeatType,
|
||||
chooseSeatObj,
|
||||
SEAT_COL_ARRAY,
|
||||
handleOk,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.order-train .order-train-main {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-train .order-train-ticket {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.order-train .order-train-ticket .order-train-ticket-main {
|
||||
color: red;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.order-tickets {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.order-tickets .ant-col {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.order-tickets .order-tickets-header {
|
||||
background-color: cornflowerblue;
|
||||
border: solid 1px cornflowerblue;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.order-tickets .order-tickets-row {
|
||||
border: solid 1px cornflowerblue;
|
||||
border-top: none;
|
||||
vertical-align: middle;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.order-tickets .choose-seat-item {
|
||||
margin: 5px 5px;
|
||||
}
|
||||
</style>
|
208
web/src/views/main/passenger.vue
Normal file
208
web/src/views/main/passenger.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<p>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleQuery()">刷新</a-button>
|
||||
<a-button type="primary" @click="onAdd">新增</a-button>
|
||||
</a-space>
|
||||
</p>
|
||||
<a-table :columns="columns"
|
||||
:dataSource="passengers"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<a-space>
|
||||
<a-popconfirm
|
||||
cancel-text="取消"
|
||||
ok-text="确认"
|
||||
title="删除后不可恢复,确认删除?" @confirm="onDelete(record)">
|
||||
<a style="color: red">删除</a>
|
||||
</a-popconfirm>
|
||||
<a @click="onEdit(record)">编辑</a>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'type'">
|
||||
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
|
||||
<span v-if="item.code === record.type">
|
||||
{{ item.desc }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-modal v-model:visible="visible" cancel-text="取消" ok-text="确认"
|
||||
title="乘车人" @ok="handleOk">
|
||||
<a-form :label-col="{span: 4}" :model="passenger" :wrapper-col="{ span: 20 }">
|
||||
<a-form-item label="姓名">
|
||||
<a-input v-model:value="passenger.name"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="身份证">
|
||||
<a-input v-model:value="passenger.idCard"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="旅客类型">
|
||||
<a-select v-model:value="passenger.type">
|
||||
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
|
||||
{{ item.desc }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, onMounted, ref} from 'vue';
|
||||
import {notification} from "ant-design-vue";
|
||||
import axios from "axios";
|
||||
|
||||
export default defineComponent({
|
||||
name: "passenger-view",
|
||||
setup() {
|
||||
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
|
||||
const visible = ref(false);
|
||||
let passenger = ref({
|
||||
id: undefined,
|
||||
memberId: undefined,
|
||||
name: undefined,
|
||||
idCard: undefined,
|
||||
type: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
});
|
||||
const passengers = ref([]);
|
||||
// 分页的三个属性名是固定的
|
||||
const pagination = ref({
|
||||
total: 0,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
let loading = ref(false);
|
||||
const columns = [
|
||||
{
|
||||
title: '会员id',
|
||||
dataIndex: 'memberId',
|
||||
key: 'memberId',
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '身份证',
|
||||
dataIndex: 'idCard',
|
||||
key: 'idCard',
|
||||
},
|
||||
{
|
||||
title: '旅客类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation'
|
||||
}
|
||||
];
|
||||
|
||||
const onAdd = () => {
|
||||
passenger.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const onEdit = (record) => {
|
||||
passenger.value = window.Tool.copy(record);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const onDelete = (record) => {
|
||||
axios.delete("/member/passenger/delete/" + record.id).then((response) => {
|
||||
const data = response.data;
|
||||
if (data.success) {
|
||||
notification.success({description: "删除成功!"});
|
||||
handleQuery({
|
||||
page: pagination.value.current,
|
||||
size: pagination.value.pageSize,
|
||||
});
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
axios.post("/member/passenger/save", passenger.value).then((response) => {
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
notification.success({description: "保存成功!"});
|
||||
visible.value = false;
|
||||
handleQuery({
|
||||
page: pagination.value.current,
|
||||
size: pagination.value.pageSize
|
||||
});
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuery = (param) => {
|
||||
if (!param) {
|
||||
param = {
|
||||
page: 1,
|
||||
size: pagination.value.pageSize
|
||||
};
|
||||
}
|
||||
loading.value = true;
|
||||
axios.get("/member/passenger/query-list", {
|
||||
params: {
|
||||
page: param.page,
|
||||
size: param.size
|
||||
}
|
||||
}).then((response) => {
|
||||
loading.value = false;
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
passengers.value = data.content.list;
|
||||
// 设置分页控件的值
|
||||
pagination.value.current = param.page;
|
||||
pagination.value.total = data.content.total;
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination) => {
|
||||
// console.log("看看自带的分页参数都有啥:" + pagination);
|
||||
handleQuery({
|
||||
page: pagination.current,
|
||||
size: pagination.pageSize
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery({
|
||||
page: 1,
|
||||
size: pagination.value.pageSize
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
PASSENGER_TYPE_ARRAY,
|
||||
passenger,
|
||||
visible,
|
||||
passengers,
|
||||
pagination,
|
||||
columns,
|
||||
handleTableChange,
|
||||
handleQuery,
|
||||
loading,
|
||||
onAdd,
|
||||
handleOk,
|
||||
onEdit,
|
||||
onDelete
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
259
web/src/views/main/ticket.vue
Normal file
259
web/src/views/main/ticket.vue
Normal file
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<p>
|
||||
<a-space>
|
||||
<a-date-picker v-model:value="params.date" placeholder="请选择日期" valueFormat="YYYY-MM-DD"></a-date-picker>
|
||||
<station-select-view v-model="params.start" width="200px"></station-select-view>
|
||||
<station-select-view v-model="params.end" width="200px"></station-select-view>
|
||||
<a-button type="primary" @click="handleQuery()">查找</a-button>
|
||||
</a-space>
|
||||
</p>
|
||||
<a-table :columns="columns"
|
||||
:dataSource="dailyTrainTickets"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<a-button type="primary" @click="toOrder(record)">预订</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'station'">
|
||||
{{ record.start }}<br/>
|
||||
{{ record.end }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'time'">
|
||||
{{ record.startTime }}<br/>
|
||||
{{ record.endTime }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'duration'">
|
||||
{{ calDuration(record.startTime, record.endTime) }}<br/>
|
||||
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
|
||||
次日到达
|
||||
</div>
|
||||
<div v-else>
|
||||
当日到达
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'ydz'">
|
||||
<div v-if="record.ydz >= 0">
|
||||
{{ record.ydz }}<br/>
|
||||
{{ record.ydzPrice }}¥
|
||||
</div>
|
||||
<div v-else>
|
||||
--
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'edz'">
|
||||
<div v-if="record.edz >= 0">
|
||||
{{ record.edz }}<br/>
|
||||
{{ record.edzPrice }}¥
|
||||
</div>
|
||||
<div v-else>
|
||||
--
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'rw'">
|
||||
<div v-if="record.rw >= 0">
|
||||
{{ record.rw }}<br/>
|
||||
{{ record.rwPrice }}¥
|
||||
</div>
|
||||
<div v-else>
|
||||
--
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'yw'">
|
||||
<div v-if="record.yw >= 0">
|
||||
{{ record.yw }}<br/>
|
||||
{{ record.ywPrice }}¥
|
||||
</div>
|
||||
<div v-else>
|
||||
--
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, onMounted, ref} from 'vue';
|
||||
import {notification} from "ant-design-vue";
|
||||
import axios from "axios";
|
||||
import StationSelectView from "@/components/station-select";
|
||||
import dayjs from "dayjs";
|
||||
import router from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ticket-view",
|
||||
components: {StationSelectView},
|
||||
setup() {
|
||||
const visible = ref(false);
|
||||
let dailyTrainTicket = ref({
|
||||
id: undefined,
|
||||
date: undefined,
|
||||
trainCode: undefined,
|
||||
start: undefined,
|
||||
startPinyin: undefined,
|
||||
startTime: undefined,
|
||||
startIndex: undefined,
|
||||
end: undefined,
|
||||
endPinyin: undefined,
|
||||
endTime: undefined,
|
||||
endIndex: undefined,
|
||||
ydz: undefined,
|
||||
ydzPrice: undefined,
|
||||
edz: undefined,
|
||||
edzPrice: undefined,
|
||||
rw: undefined,
|
||||
rwPrice: undefined,
|
||||
yw: undefined,
|
||||
ywPrice: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
});
|
||||
const dailyTrainTickets = ref([]);
|
||||
// 分页的三个属性名是固定的
|
||||
const pagination = ref({
|
||||
total: 0,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
let loading = ref(false);
|
||||
const params = ref({});
|
||||
const columns = [
|
||||
{
|
||||
title: '车次编号',
|
||||
dataIndex: 'trainCode',
|
||||
key: 'trainCode',
|
||||
},
|
||||
{
|
||||
title: '车站',
|
||||
dataIndex: 'station',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
},
|
||||
{
|
||||
title: '历时',
|
||||
dataIndex: 'duration',
|
||||
},
|
||||
{
|
||||
title: '一等座',
|
||||
dataIndex: 'ydz',
|
||||
key: 'ydz',
|
||||
},
|
||||
{
|
||||
title: '二等座',
|
||||
dataIndex: 'edz',
|
||||
key: 'edz',
|
||||
},
|
||||
{
|
||||
title: '软卧',
|
||||
dataIndex: 'rw',
|
||||
key: 'rw',
|
||||
},
|
||||
{
|
||||
title: '硬卧',
|
||||
dataIndex: 'yw',
|
||||
key: 'yw',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const handleQuery = (param) => {
|
||||
if (Tool.isEmpty(params.value.date)) {
|
||||
notification.error({description: "请输入日期"});
|
||||
return;
|
||||
}
|
||||
if (Tool.isEmpty(params.value.start)) {
|
||||
notification.error({description: "请输入出发地"});
|
||||
return;
|
||||
}
|
||||
if (Tool.isEmpty(params.value.end)) {
|
||||
notification.error({description: "请输入目的地"});
|
||||
return;
|
||||
}
|
||||
if (!param) {
|
||||
param = {
|
||||
page: 1,
|
||||
size: pagination.value.pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// 保存查询参数
|
||||
SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
|
||||
|
||||
loading.value = true;
|
||||
axios.get("/business/daily-train-ticket/query-list", {
|
||||
params: {
|
||||
page: param.page,
|
||||
size: param.size,
|
||||
trainCode: params.value.trainCode,
|
||||
date: params.value.date,
|
||||
start: params.value.start,
|
||||
end: params.value.end
|
||||
}
|
||||
}).then((response) => {
|
||||
loading.value = false;
|
||||
let data = response.data;
|
||||
if (data.success) {
|
||||
dailyTrainTickets.value = data.content.list;
|
||||
// 设置分页控件的值
|
||||
pagination.value.current = param.page;
|
||||
pagination.value.total = data.content.total;
|
||||
} else {
|
||||
notification.error({description: data.message});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (page) => {
|
||||
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
|
||||
pagination.value.pageSize = page.pageSize;
|
||||
handleQuery({
|
||||
page: page.current,
|
||||
size: page.pageSize
|
||||
});
|
||||
};
|
||||
|
||||
const calDuration = (startTime, endTime) => {
|
||||
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
|
||||
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
|
||||
};
|
||||
|
||||
const toOrder = (record) => {
|
||||
dailyTrainTicket.value = Tool.copy(record);
|
||||
SessionStorage.set(SESSION_ORDER, dailyTrainTicket.value);
|
||||
router.push("/order")
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// "|| {}"是常用技巧,可以避免空指针异常
|
||||
params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
|
||||
if (Tool.isNotEmpty(params.value)) {
|
||||
handleQuery({
|
||||
page: 1,
|
||||
size: pagination.value.pageSize
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dailyTrainTicket,
|
||||
visible,
|
||||
dailyTrainTickets,
|
||||
pagination,
|
||||
columns,
|
||||
handleTableChange,
|
||||
handleQuery,
|
||||
loading,
|
||||
params,
|
||||
calDuration,
|
||||
toOrder
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
14
web/src/views/main/welcome.vue
Normal file
14
web/src/views/main/welcome.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<h1>欢迎使用12306售票系统</h1>
|
||||
</template>
|
||||
<script>
|
||||
import {defineComponent} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user