提交 9b9c9974 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 新增考试

上级 a3c66084
...@@ -2,7 +2,7 @@ import httpRequest from '@/utils/axios' ...@@ -2,7 +2,7 @@ import httpRequest from '@/utils/axios'
// 获取用户信息 // 获取用户信息
export function getUser() { export function getUser() {
return httpRequest.get('/api/passport/account/get-user-info') return httpRequest.get('/api/psp/v1/my/info')
} }
// 退出登录 // 退出登录
......
import httpRequest from '@/utils/axios'
// 获取消息列表
export function getExamList(params?: { type: number }) {
return httpRequest.get('/api/psp/v1/exam/list', { params })
}
// 获取文档数据····
export function chooseExam(data: {
exam_id: string
id_number: string
address: string
company: string
position: string
need_receive: string
}) {
return httpRequest.post('/api/psp/v1/exam/choose', data)
}
<script setup lang="ts">
import { chooseExam } from '../api'
import { useUserStore } from '@/stores/user'
import type { ExamType } from '../types'
import { Toast } from 'vant'
const userStore = useUserStore()
interface Props {
data: ExamType
}
const props = defineProps<Props>()
const emit = defineEmits(['update', 'update:show'])
const form = reactive({ id_number: '', address: '', company: '', position: '', need_receive: '1' })
function onSubmit() {
const params = { ...form, exam_id: props.data.exam_id }
chooseExam(params).then((res: any) => {
if (res.code === 0) {
emit('update')
emit('update:show', false)
} else {
Toast.fail(res.message)
}
})
}
</script>
<template>
<van-dialog :show-confirm-button="false">
<van-icon name="cross" @click="$emit('update:show', false)" />
<van-form @submit="onSubmit" class="exam-form">
<van-field label="姓名">
<template #input>{{ userStore.user?.name }}</template>
</van-field>
<van-field label="手机号">
<template #input>{{ userStore.user?.mobile }}</template>
</van-field>
<van-field
v-model="form.company"
type="textarea"
label="公司名称"
:rules="[{ required: true, message: '请输入' }]"
/>
<van-field v-model="form.position" type="text" label="职位" :rules="[{ required: true, message: '请输入' }]" />
<van-field
v-model="form.id_number"
type="text"
label="身份证号"
:rules="[{ required: true, message: '请输入' }]"
/>
<van-field
v-model="form.address"
type="textarea"
label="邮寄地址"
:rules="[{ required: true, message: '请输入' }]"
/>
<p class="t1">以上信息仅用于考试身份的确认与证书的发放。</p>
<p class="t2">是否领取纸板《PRP陪伴手册》</p>
<van-radio-group v-model="form.need_receive">
<van-radio name="1"></van-radio>&nbsp;&nbsp;&nbsp;&nbsp;
<van-radio name="0"></van-radio>
</van-radio-group>
<van-button block round native-type="submit" class="my-button">确定</van-button>
</van-form>
</van-dialog>
</template>
<style lang="scss" scoped>
.van-icon-cross {
position: absolute;
right: 0.3rem;
top: 0.4rem;
font-size: 0.26rem;
}
.exam-form {
margin-top: 0.4rem;
padding: 0.4rem 0.3rem;
--van-field-label-width: 4em;
}
.van-cell {
padding: 0.15rem 0;
}
.van-cell:after {
display: none;
}
:deep(.van-field__control) {
padding: 0.1rem;
line-height: 0.5rem;
background: #f5f5f5;
border-radius: 0.16rem;
}
.van-radio-group {
display: flex;
}
.my-button {
margin-top: 0.55rem;
}
.t1 {
font-size: 0.24rem;
font-weight: 400;
line-height: 0.33rem;
color: #909090;
}
.t2 {
margin: 0.22rem 0;
font-size: 0.28rem;
font-weight: 400;
line-height: 0.4rem;
color: #333333;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/exam',
component: AppLayout,
meta: { requireLogin: true },
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/View.vue'), props: true }
]
}
]
export interface ExamType {
exam_id: string
name: string
start_time: string
end_time: string
choose: boolean
score: string
examinee_number: string
created_time: string
}
<script setup lang="ts">
import dayjs from 'dayjs'
import { getExamList } from '../api'
import type { ExamType } from '../types'
import FormDialog from '../components/FormDialog.vue'
interface Info {
address: string
can_choose: boolean
id_number: string
list: ExamType[]
}
const router = useRouter()
const tabActive = $ref<number>(2)
const dataset = reactive<Info>({ address: '', can_choose: true, id_number: '', list: [] })
// 日期转换
function formatDate(startTime: string, endTime: string) {
return dayjs(startTime).format('YYYY年MM月DD日 hh:mm') + '-' + dayjs(endTime).format('hh:mm')
}
// 获取列表
const fetchList = () => {
getExamList({ type: tabActive }).then(res => {
Object.assign(dataset, res.data)
})
}
onMounted(() => {
fetchList()
})
let dialogShow = $ref<boolean>(false)
let activeExam = $ref<ExamType>()
function handleClick(data: ExamType) {
if (data.choose) {
router.push(`/exam/${data.exam_id}`)
}
if (!dataset.can_choose) return
activeExam = data
dialogShow = true
}
</script>
<template>
<AppContainer title="考试系统">
<van-tabs
v-model:active="tabActive"
shrink
background="transparent"
title-active-color="#033974"
title-inactive-color="#4E4E4E"
line-height="0"
@change="fetchList"
>
<van-tab title="已选" :name="2"></van-tab>
<van-tab title="未选" :name="1">
<p class="tips">请选择以下一场考试。每人一次正式考试机会,一次补考机会。</p></van-tab
>
</van-tabs>
<div class="exam-list" v-if="dataset.list.length">
<div class="exam-item" v-for="item in dataset.list" :key="item.exam_id" @click="handleClick(item)">
<h3 class="exam-item__name">{{ item.name }}</h3>
<p class="exam-item__time">考试时间:{{ formatDate(item.start_time, item.end_time) }}</p>
</div>
</div>
<van-empty description="暂无数据" v-else />
</AppContainer>
<FormDialog :data="activeExam" v-model:show="dialogShow" @update="fetchList"></FormDialog>
</template>
<style lang="scss" scoped>
:deep(.van-tabs__nav) {
padding: 0 !important;
}
:deep(.van-tab) {
padding: 0 15px;
}
:deep(.van-tab + .van-tab) {
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
width: 1px;
height: 12px;
background-color: #fff;
}
}
.tips {
color: #033974;
font-size: 0.24rem;
font-weight: 400;
padding: 0.2rem 0;
text-align: center;
}
.exam-item {
padding: 0.3rem 0.45rem;
background-color: #fff;
border-radius: 0.2rem;
margin-bottom: 0.2rem;
cursor: pointer;
}
.exam-item-hd {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.1rem;
line-height: 0.4rem;
}
.exam-item__name {
font-size: 0.28rem;
font-weight: 400;
color: #333;
}
.exam-item__time {
margin-top: 0.07rem;
font-size: 0.24rem;
color: #999;
}
</style>
<script setup lang="ts">
import dayjs from 'dayjs'
import { getExamList } from '../api'
import type { ExamType } from '../types'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
interface Info {
address: string
can_choose: boolean
id_number: string
list: ExamType[]
}
const props = defineProps<{ id: string }>()
const dataset = reactive<Info>({ address: '', can_choose: true, id_number: '', list: [] })
// 日期转换
function formatDate(time: string) {
return dayjs(time).format('YYYY年MM月DD日 hh:mm:ss')
}
// 获取列表
const fetchList = () => {
getExamList().then(res => {
Object.assign(dataset, res.data)
})
}
onMounted(() => {
fetchList()
})
const data = $computed((): ExamType | Record<string, any> => {
return dataset.list.find(item => item.exam_id === props.id) || {}
})
const hasScore = computed<boolean>(() => {
return parseInt(data.score) >= 0
})
const examUrl = computed<string>(() => {
return `https://x-exam.ezijing.com/login/${props.id}`
})
</script>
<template>
<AppContainer title="成绩查看" v-if="hasScore">
<div class="box">
<h2 class="t0">{{ userStore.user?.name }}</h2>
<p class="t1">你的成绩</p>
<p class="t2">
<span>{{ data.score }}</span>
<em></em>
</p>
<dl>
<dt>考试名称</dt>
<dd>{{ data.name }}</dd>
</dl>
<dl>
<dt>准考证号</dt>
<dd>{{ data.examinee_number }}</dd>
</dl>
<dl>
<dt>考试时间</dt>
<dd>{{ formatDate(data.start_time) }}</dd>
</dl>
</div>
</AppContainer>
<AppContainer title="考场信息" v-else>
<div class="box">
<dl>
<dt>姓名</dt>
<dd>{{ userStore.user?.name }}</dd>
</dl>
<dl>
<dt>准考证号</dt>
<dd>{{ data.examinee_number }}</dd>
</dl>
<dl>
<dt>考试时间</dt>
<dd>{{ formatDate(data.start_time) }}</dd>
</dl>
<dl>
<dt>考试地址</dt>
<dd>{{ examUrl }}</dd>
</dl>
</div>
<p class="tips">
提示:<br />
可提前30分钟进入考试系统,<br />
考试时间到了,仍未出现试题,请退出重新登录。<br />
建议考试设备为电脑,保持网络稳定。
</p>
<a :href="examUrl" target="_blank">
<van-button block round native-type="submit" class="my-button">点击开考</van-button>
</a>
</AppContainer>
</template>
<style lang="scss" scoped>
.box {
padding: 0.5rem;
background-color: #fff;
border-radius: 0.3rem;
dl {
display: flex;
}
dl + dl {
margin-top: 0.42rem;
}
dt {
width: 4em;
margin-right: 0.4rem;
font-size: 0.28rem;
font-weight: 400;
line-height: 0.4rem;
color: #666666;
white-space: nowrap;
}
dd {
flex: 1;
font-size: 0.3rem;
font-weight: 400;
line-height: 0.4rem;
color: #333;
word-break: break-all;
}
}
.tips {
margin: 0.54rem 0;
font-size: 0.26rem;
line-height: 0.46rem;
color: #033974;
}
.t0 {
font-size: 0.46rem;
font-weight: 500;
color: #033974;
text-align: center;
}
.t1 {
margin-top: 0.52rem;
font-size: 0.32rem;
font-weight: 400;
color: #333333;
text-align: center;
}
.t2 {
padding: 0.5rem 0;
font-size: 0.4rem;
color: #e6a74d;
text-align: center;
span {
font-size: 1rem;
}
}
</style>
...@@ -18,24 +18,37 @@ function handleViewDoc(data: IDocItem) { ...@@ -18,24 +18,37 @@ function handleViewDoc(data: IDocItem) {
<template> <template>
<AppCard title="考试攻略" id="exam"> <AppCard title="考试攻略" id="exam">
<div class="box"> <div class="exam">
<h2>解释文档</h2> <div class="box box-doc">
<ul v-if="docs.length"> <h2>解释文档</h2>
<li v-for="item in docs" :key="item.id" @click="handleViewDoc(item)"> <ul v-if="docs.length">
<p>{{ item.title }}</p> <li v-for="item in docs" :key="item.id" @click="handleViewDoc(item)">
<span>{{ item.pv }}</span> <p>{{ item.title }}</p>
<van-icon name="arrow" /> <span>{{ item.pv }}</span>
</li> <van-icon name="arrow" />
</ul> </li>
<van-empty description="暂无内容" v-else /> </ul>
<van-empty description="暂无内容" v-else />
</div>
<div class="box box-exam">
<h2>考试系统</h2>
<router-link to="/exam" class="box-exam-content">
<img src="https://webapp-pub.ezijing.com/project/prp-h5/exam_bg.png" />
<p class="t2">去看看</p>
</router-link>
</div>
</div> </div>
</AppCard> </AppCard>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.exam {
display: flex;
justify-content: space-between;
}
.box { .box {
margin-top: 0.28rem;
padding: 0.2rem; padding: 0.2rem;
width: 3.33rem;
background: #fff; background: #fff;
border-radius: 0.2rem; border-radius: 0.2rem;
overflow: hidden; overflow: hidden;
...@@ -46,26 +59,21 @@ function handleViewDoc(data: IDocItem) { ...@@ -46,26 +59,21 @@ function handleViewDoc(data: IDocItem) {
font-weight: 600; font-weight: 600;
color: #4e4e4e; color: #4e4e4e;
} }
// ul { }
// display: grid; .box-doc {
// grid-template-columns: 1fr 1fr; height: 3.43rem;
// column-gap: 0.2rem; background: #fff url(https://webapp-pub.ezijing.com/project/prp-h5/query_bg_1.png) no-repeat center bottom;
// row-gap: 0.2rem; background-size: 100% auto;
// } ul {
overflow: hidden;
}
li { li {
height: 0.75rem; margin: 0.3rem 0 0;
padding: 0 0.14rem;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.2rem; font-size: 0.2rem;
color: #adadad; color: #adadad;
background: #f7f7f7;
border-radius: 0.1rem;
cursor: pointer; cursor: pointer;
overflow: hidden;
}
li + li {
margin-top: 0.2rem;
} }
p { p {
flex: 1; flex: 1;
...@@ -79,7 +87,26 @@ function handleViewDoc(data: IDocItem) { ...@@ -79,7 +87,26 @@ function handleViewDoc(data: IDocItem) {
min-width: 0.44rem; min-width: 0.44rem;
font-size: 0.2rem; font-size: 0.2rem;
color: #c6c6c6; color: #c6c6c6;
padding: 0 0.1rem; }
}
.box-exam {
.t2 {
display: inline-block;
padding: 0 0.2rem;
margin-top: 0.35rem;
font-size: 0.22rem;
line-height: 0.4rem;
color: #ffffff;
background-color: #033974;
}
}
.box-exam-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
img {
width: 1.61rem;
} }
} }
</style> </style>
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUser, logout } from '@/api/base' import { getUser, logout } from '@/api/base'
interface IUserState { import type { IUserState } from '@/types'
id: string
avatar: string interface State {
mobile: string user: IUserState | null
realname: string
nickname: string
username: string
} }
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'user', id: 'user',
state: () => { state: (): State => {
return { return {
user: null as IUserState | null user: null
} }
}, },
getters: { getters: {
isLogin: state => !!state.user, isLogin: state => !!state.user
userName: ({ user }) => {
if (!user) return ''
return user.realname || user.nickname || user.username || ''
}
}, },
actions: { actions: {
async getUser() { async getUser() {
const res = await getUser() const res = await getUser()
this.user = res.data this.user = res.data.info
}, },
async logout() { async logout() {
await logout() await logout()
......
// 用户信息
export interface IUserState {
address: string
avatar: string
batch: string
brief: string
certificate_number: string
certificate_url: string
company: string
created_operator: string
created_time: string
id: string
id_number: string
label: string[]
mobile: string
name: string
need_receive: string
notice_url: string
position: string
sso_id: string
star: string
status: string
student_id: string
type: string
updated_operator: string
updated_time: string
}
// 视频 // 视频
export interface IVideoItem { export interface IVideoItem {
id: string id: string
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论