添加就餐相关页面

This commit is contained in:
ywyonui 2025-08-06 13:22:26 +08:00
parent 9ffab44ca4
commit a27e4e8a40
12 changed files with 2230 additions and 0 deletions

92
src/api/base/jcApi.ts Normal file
View File

@ -0,0 +1,92 @@
import { get, post } from "@/utils/request";
/**
*
*/
export const jcGetJcBzListApi = async (params: any) => {
return await get("/mobile/jc/getJcBzList", params);
};
/**
*
*/
export const jcGetJcBzDetailApi = async (params: any) => {
return await get("/mobile/jc/getJcBzDetail", params);
};
/**
*
*/
export const jcBmJcBzApi = async (params: any) => {
return await post("/mobile/jc/bmJcBz", params);
};
/**
*
*/
export const jcGetXsBmJcBzListApi = async (params: any) => {
return await get("/mobile/jc/getXsBmJcBzList", params);
};
/**
*
*/
export const jcCancelBmJcBzApi = async (params: any) => {
return await post("/mobile/jc/cancelBmJcBz", params);
};
/**
*
*/
export const jcGetBmExpiredTimeApi = async (params: any) => {
return await get("/mobile/jc/getBmExpiredTime", params);
};
/**
*
*/
export const jcFqJcBzJfjApi = async (params: any) => {
return await post("/mobile/jc/fqJcBzJfj", params);
};
/**
*
*/
export const jcJcBzJfCxjApi = async (params: any) => {
return await post("/mobile/jc/jcBzJfcx", params);
};
/**
*
*/
export const jcGetJcBzBmXsListApi = async (params: any) => {
return await get("/mobile/jc/getJcBzBmXsList", params);
};
/**
*
*/
export const jcGetJcBzTjInfoApi = async (params: any) => {
return await get("/mobile/jc/getJcBzTjInfo", params);
};
/**
*
*/
export const jcGetDqXqJcBzConfigApi = async () => {
return await get("/mobile/jc/getDqXqJcBzConfig");
};
/**
*
*/
export const jcGetXsJcJlApi = async (params: any) => {
return await get("/mobile/jc/getXsJcJl", params);
};
/**
*
*/
export const jcGetJcBzPayExpiredTimeApi = async (params: any) => {
return await get("/mobile/jc/getJcBzPayExpiredTime", params);
};

View File

@ -0,0 +1,265 @@
<template>
<view>
<!-- 就餐标准网格列表 -->
<view class="jc-bz-grid" v-if="jcBzList.length > 0">
<view
v-for="(jcBz, index) in jcBzList"
:key="jcBz.id || index"
class="jc-bz-item"
:class="{ selected: jcBz.isSelected }"
@click="toggleSelection(jcBz)"
>
<view class="jc-bz-header">
<view class="jc-bz-name">{{ jcBz.bzmc }}</view>
<view class="detail-btn" @click.stop="goToDetail(jcBz)">
<image src="/static/base/home/details.svg" class="detail-icon" />
</view>
</view>
<view class="jc-bz-info">
<view class="jc-bz-price">
<text>价格</text>
<text class="price-value">¥{{ jcBz.jfje }}</text>
</view>
<view class="jc-bz-desc">{{ jcBz.bzms || '暂无描述' }}</view>
</view>
<view v-if="jcBz.isSelected" class="selected-mark">
<uni-icons
type="checkbox-filled"
color="#3FBF72"
size="22"
></uni-icons>
</view>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-else class="empty-jc-bz-list">
<view class="empty-icon">
<u-icon name="list" size="50" color="#C8C9CC"></u-icon>
</view>
<view class="empty-text">暂无就餐标准数据</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { useDataStore } from "@/store/modules/data";
import { jcGetJcBzListApi } from "@/api/base/jcApi";
const { setJcBzData } = useDataStore();
//
const props = withDefaults(defineProps<{
xsId: string,
canSelected: boolean,
multiple: boolean,
}>(), {
xsId: '',
canSelected: false,
multiple: false,
});
// emit
const emit = defineEmits(['change'])
//
const jcBzList = ref<any>([]);
//
const getJcBzList = async () => {
try {
const res = await jcGetJcBzListApi({
xsId: props.xsId
});
if (res.resultCode === 1) {
jcBzList.value = res.result || [];
//
initSelectedStatus();
}
} catch (error) {
console.error('获取就餐标准列表失败:', error);
}
};
//
const initSelectedStatus = () => {
// ID
let selectedJcBzIds = uni.getStorageSync("selectedJcBzIds") || [];
jcBzList.value.forEach((jcBz: any) => {
jcBz.isSelected = selectedJcBzIds.includes(jcBz.id);
});
};
//
const toggleSelection = (jcBz: any) => {
if (!props.canSelected) {
return;
}
// ID
let selectedJcBzIds = uni.getStorageSync("selectedJcBzIds") || [];
if (jcBz.isSelected) {
jcBz.isSelected = false;
if (props.multiple) {
//
selectedJcBzIds = selectedJcBzIds.filter(
(id: string) => id !== jcBz.id
);
} else {
//
selectedJcBzIds = [];
}
} else {
if (props.multiple) {
//
if (!selectedJcBzIds.includes(jcBz.id)) {
selectedJcBzIds.push(jcBz.id);
}
} else {
//
selectedJcBzIds = [jcBz.id];
//
jcBzList.value.forEach((jcBz: any) => {
jcBz.isSelected = false;
});
}
jcBz.isSelected = true;
}
//
uni.setStorageSync("selectedJcBzIds", selectedJcBzIds);
emit("change", selectedJcBzIds);
}
const goToDetail = (jcBz: any) => {
setJcBzData(jcBz);
uni.navigateTo({
url: `/pages/base/jc/detail`,
});
};
// ID
watch(() => props.xsId, (newVal) => {
if (newVal) {
getJcBzList();
}
});
//
onMounted(() => {
if (props.xsId) {
getJcBzList();
}
});
</script>
<style lang="scss" scoped>
.jc-bz-grid {
display: flex;
flex-wrap: wrap;
padding: 15px 15px 0 15px;
.jc-bz-item {
position: relative;
width: calc(50% - 10px);
margin-bottom: 15px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
box-sizing: border-box;
border: 1px solid transparent;
transition: all 0.3s ease;
&:nth-child(odd) {
margin-right: 10px;
}
&:nth-child(even) {
margin-left: 10px;
}
&.selected {
border: 1px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.05);
box-shadow: 0 2px 8px rgba(63, 191, 114, 0.15);
}
.jc-bz-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
.jc-bz-name {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 10px;
}
.detail-btn {
flex-shrink: 0;
padding: 4px;
.detail-icon {
width: 20px;
height: 20px;
}
}
}
.jc-bz-info {
.jc-bz-price {
font-size: 14px;
color: #666;
margin-bottom: 8px;
.price-value {
color: #ff6b00;
font-weight: bold;
}
}
.jc-bz-desc {
font-size: 12px;
color: #999;
line-height: 1.4;
}
}
.selected-mark {
position: absolute;
top: -6px;
right: -6px;
}
}
}
//
.empty-jc-bz-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<view>
<!-- 就餐记录列表 -->
<view class="jc-record-list" v-if="jcRecordList.length > 0">
<view
v-for="(record, index) in jcRecordList"
:key="record.id || index"
class="record-item"
>
<view class="record-header">
<view class="record-date">{{ formatDate(record.jcDate) }}</view>
<view class="record-status" :class="getStatusClass(record.status)">
{{ getStatusText(record.status) }}
</view>
</view>
<view class="record-content">
<view class="record-info">
<view class="record-bz">{{ record.bzmc }}</view>
<view class="record-time">{{ record.jcTime }}</view>
</view>
<view class="record-price">¥{{ record.jfje }}</view>
</view>
<view class="record-footer">
<view class="record-location">{{ record.jcLocation || '暂无地点信息' }}</view>
</view>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-else class="empty-record-list">
<view class="empty-icon">
<u-icon name="list" size="50" color="#C8C9CC"></u-icon>
</view>
<view class="empty-text">暂无就餐记录</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { jcGetXsJcJlApi } from "@/api/base/jcApi";
//
const props = withDefaults(defineProps<{
xsId: string,
}>(), {
xsId: '',
});
//
const jcRecordList = ref<any>([]);
//
const getJcRecordList = async () => {
try {
const res = await jcGetXsJcJlApi({
xsId: props.xsId
});
if (res.resultCode === 1) {
jcRecordList.value = res.result || [];
}
} catch (error) {
console.error('获取就餐记录失败:', error);
}
};
//
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
//
const getStatusClass = (status: string) => {
switch (status) {
case '1': return 'status-success';
case '2': return 'status-pending';
case '3': return 'status-cancelled';
default: return 'status-default';
}
};
//
const getStatusText = (status: string) => {
switch (status) {
case '1': return '已完成';
case '2': return '进行中';
case '3': return '已取消';
default: return '未知状态';
}
};
// ID
watch(() => props.xsId, (newVal) => {
if (newVal) {
getJcRecordList();
}
});
//
onMounted(() => {
if (props.xsId) {
getJcRecordList();
}
});
</script>
<style lang="scss" scoped>
.jc-record-list {
padding: 15px;
.record-item {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.record-date {
font-size: 14px;
color: #666;
}
.record-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
&.status-success {
background-color: #f0f9ff;
color: #3fbf72;
}
&.status-pending {
background-color: #fff7e6;
color: #ff8c00;
}
&.status-cancelled {
background-color: #fff2f0;
color: #ff4d4f;
}
&.status-default {
background-color: #f5f5f5;
color: #999;
}
}
}
.record-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.record-info {
.record-bz {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 5px;
}
.record-time {
font-size: 14px;
color: #666;
}
}
.record-price {
font-size: 16px;
color: #ff6b00;
font-weight: bold;
}
}
.record-footer {
.record-location {
font-size: 12px;
color: #999;
}
}
}
}
//
.empty-record-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
}
</style>

223
src/pages/base/jc/bm.vue Normal file
View File

@ -0,0 +1,223 @@
<template>
<view class="jc-bm-page">
<!-- 报名信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<!-- 学生选择部分 -->
<XsPicker :is-bar="true" />
</view>
</view>
<!-- 可滚动的内容区域 -->
<view class="scrollable-content">
<JcBzList :xs-id="curXs.id" :can-selected="true" :multiple="true" @change="onJcBzChange" />
</view>
<!-- 底部操作区域 -->
<view class="bottom-action">
<view class="selected-info">
<text>已选择{{ selectedCount }} 个就餐标准</text>
<text class="total-price">总金额¥{{ totalPrice }}</text>
</view>
<view class="action-buttons">
<view class="cancel-btn" @click="goBack">取消</view>
<view class="confirm-btn" @click="confirmBm" :class="{ disabled: selectedCount === 0 }">确认报名</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import JcBzList from "@/pages/base/components/JcBzList/index.vue"
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jcBmJcBzApi } from "@/api/base/jcApi";
const { getCurXs, getUser } = useUserStore();
const { getData, setData } = useDataStore();
const curXs = computed(() => getCurXs);
// ID
const selectedJcBzIds = ref<string[]>([]);
//
const selectedJcBzList = ref<any[]>([]);
//
const selectedCount = computed(() => selectedJcBzIds.value.length);
//
const totalPrice = computed(() => {
let total = 0;
selectedJcBzList.value.forEach(jcBz => {
total += jcBz.jfje || 0;
});
return total;
});
//
const onJcBzChange = (ids: string[]) => {
selectedJcBzIds.value = ids;
// ID
//
updateSelectedJcBzList();
};
//
const updateSelectedJcBzList = () => {
// selectedJcBzIds
// 使
selectedJcBzList.value = [];
};
//
const goBack = () => {
uni.navigateBack();
};
//
const confirmBm = async () => {
if (selectedCount.value === 0) {
uni.showToast({
title: "请选择就餐标准",
icon: "none",
});
return;
}
try {
const res = await jcBmJcBzApi({
xsId: curXs.value.id,
jcBzIds: selectedJcBzIds.value,
jzId: getUser.jzId,
userId: getUser.userId,
});
if (res.resultCode === 1) {
uni.showToast({
title: "报名成功",
icon: "success",
});
// store
setData({
...getData,
xsId: curXs.value.id,
jcBzIds: selectedJcBzIds.value,
jcBzList: selectedJcBzList.value,
totalJe: totalPrice.value,
});
//
uni.redirectTo({
url: '/pages/base/jc/pay/index'
});
} else {
uni.showToast({
title: res.result?.message || "报名失败",
icon: "none",
});
}
} catch (error) {
console.error('报名失败:', error);
uni.showToast({
title: "报名失败,请重试",
icon: "none",
});
}
};
//
onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.jc-bm-page {
min-height: 100%;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
}
}
//
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // iOS
}
.bottom-action {
background-color: #fff;
padding: 15px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.selected-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
font-size: 14px;
color: #666;
.total-price {
color: #ff6b00;
font-weight: bold;
font-size: 16px;
}
}
.action-buttons {
display: flex;
justify-content: space-between;
.cancel-btn,
.confirm-btn {
width: 48%;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 22px;
font-size: 16px;
}
.cancel-btn {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
.confirm-btn {
background-color: #2879ff;
color: #fff;
&.disabled {
background-color: #ccc;
color: #999;
}
}
}
}
</style>

View File

@ -0,0 +1,285 @@
<template>
<BasicLayout>
<view class="jc-detail">
<!-- 就餐标准信息卡片 -->
<view class="info-card">
<view class="card-title">就餐标准信息</view>
<view class="divider"></view>
<view class="jc-info">
<image
class="jc-image"
:src="jcDetail.lxtp"
mode="aspectFill"
></image>
<view class="jc-content">
<view class="jc-name">{{ jcDetail.bzmc }}</view>
<view class="jc-price">
价格<text class="price-value">¥{{ jcDetail.jfje }}</text>
</view>
<view class="jc-desc">{{ jcDetail.bzms || '暂无描述' }}</view>
<view class="jc-time">有效期{{ jcDetail.yxq || '暂无有效期信息' }}</view>
</view>
</view>
</view>
<!-- 就餐标准详情 -->
<view class="info-card">
<view class="card-title">标准详情</view>
<view class="divider"></view>
<view class="content-section">
<text>{{ jcDetail.bzms || "暂无详细信息" }}</text>
</view>
</view>
<!-- 使用说明 -->
<view class="info-card">
<view class="card-title">使用说明</view>
<view class="divider"></view>
<view class="content-section">
<template v-if="usageInstructions && usageInstructions.length > 0">
<view
v-for="(instruction, index) in usageInstructions"
:key="index"
class="instruction-item"
>
<text>{{ instruction }}</text>
</view>
</template>
<template v-else>
<view class="empty-data">
<u-icon name="info-circle" color="#C8C9CC" size="18"></u-icon>
<text>暂无使用说明</text>
</view>
</template>
</view>
</view>
<!-- 注意事项 -->
<view class="info-card">
<view class="card-title">注意事项</view>
<view class="divider"></view>
<view class="content-section">
<template v-if="precautions && precautions.length > 0">
<view
v-for="(precaution, index) in precautions"
:key="index"
class="precaution-item"
>
<text>{{ precaution }}</text>
</view>
</template>
<template v-else>
<view class="empty-data">
<u-icon name="info-circle" color="#C8C9CC" size="18"></u-icon>
<text>暂无注意事项</text>
</view>
</template>
</view>
</view>
</view>
<template #bottom>
<view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="返回"
class="ml-15 mr-7"
:plain="true"
@click="navigateBack"
/>
</view>
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { navigateBack } from "@/utils/uniapp";
import { useDataStore } from "@/store/modules/data";
import { storeToRefs } from "pinia";
import { imagUrl } from "@/utils";
import { jcGetJcBzDetailApi } from "@/api/base/jcApi";
//
interface JcBzData {
id?: string;
bzmc?: string;
jfje?: number;
bzms?: string;
yxq?: string;
lxtp?: string;
[key: string]: any; //
}
const useData = useDataStore();
const { jcBzData } = storeToRefs(useData);
// 使 -
const usageInstructions = ref<string[]>([]);
// -
const precautions = ref<string[]>([]);
const jcBzDetail = jcBzData.value as JcBzData;
if (jcBzDetail && jcBzDetail.id) {
jcGetJcBzDetailApi({
jcBzId: jcBzDetail.id,
}).then((res) => {
if (res.resultCode == 1) {
//
console.log('就餐标准详情:', res.result);
}
});
}
//
const jcDetail = computed(() => {
const data = (jcBzData.value as JcBzData) || {};
return {
id: data.id || "",
bzmc: data.bzmc || "暂无标准名称",
jfje: data.jfje || 0,
bzms: data.bzms || "暂无描述",
yxq: data.yxq || "暂无有效期信息",
lxtp: imagUrl(data.lxtp || ''), // 使imagUrl
};
});
// 使
const parseInstructions = () => {
const data = (jcBzData.value as JcBzData) || {};
const bzms = data.bzms || '';
// 使
// 使
if (bzms) {
//
// 使""
const parts = bzms.split('注意事项:');
if (parts.length > 1) {
usageInstructions.value = parts[0].split('\n').filter(item => item.trim());
precautions.value = parts[1].split('\n').filter(item => item.trim());
} else {
usageInstructions.value = bzms.split('\n').filter(item => item.trim());
}
}
};
onMounted(() => {
parseInstructions();
});
</script>
<style lang="scss" scoped>
.jc-detail {
background-color: #f5f7fa;
}
.info-card {
margin: 15px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.divider {
height: 1px;
background-color: #eee;
margin-bottom: 15px;
}
}
.jc-info {
display: flex;
.jc-image {
width: 120px;
height: 138px;
border-radius: 8px;
margin-right: 15px;
}
.jc-content {
flex: 1;
.jc-name {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.jc-price {
font-size: 14px;
color: #666;
margin-bottom: 8px;
.price-value {
color: #ff6b00;
font-weight: bold;
}
}
.jc-desc {
font-size: 14px;
color: #666;
margin-bottom: 8px;
line-height: 1.4;
}
.jc-time {
font-size: 14px;
color: #666;
}
}
}
.content-section {
font-size: 14px;
color: #666;
line-height: 1.6;
.instruction-item,
.precaution-item {
margin-bottom: 10px;
padding-left: 15px;
position: relative;
&:before {
content: "•";
position: absolute;
left: 0;
color: #2879ff;
}
&:last-child {
margin-bottom: 0;
}
}
.empty-data {
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
color: #909399;
font-size: 14px;
text {
margin-left: 8px;
}
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<view class="jc-page">
<!-- 就餐信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<!-- 学生选择部分 -->
<XsPicker :is-bar="true" />
</view>
</view>
<!-- 可滚动的内容区域 -->
<view class="scrollable-content">
<JcBzList :xs-id="curXs.id" />
</view>
</view>
</template>
<script setup lang="ts">
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import JcBzList from "@/pages/base/components/JcBzList/index.vue"
import { useUserStore } from "@/store/modules/user";
const { getCurXs } = useUserStore();
const curXs = computed(() => getCurXs);
//
onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.jc-page {
min-height: 100%;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
}
}
//
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // iOS
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<view class="payment-fail">
<!-- 失败提示卡片 -->
<view class="fail-card">
<view class="fail-icon-container">
<image
class="fail-icon"
src="/static/base/2222.png"
mode="aspectFit"
></image>
</view>
<view class="fail-text">抱歉报名失败</view>
<view class="divider"></view>
<view class="tip-text">请点击下方按钮</view>
<view class="finger-icon">
<image
src="/static/base/33333.png"
mode="aspectFit"
></image>
</view>
<view class="action-btn" @click="reselect">
重新报名
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//
const reselect = () => {
uni.redirectTo({
url: '/pages/base/jc/bm'
});
};
onMounted(() => {
//
console.log('支付失败页面加载');
});
</script>
<style lang="scss" scoped>
.payment-fail {
min-height: 100%;
background-color: #f5f7fa;
}
.fail-card {
margin: 15px;
width: 90%;
background-color: #fff;
border-radius: 8px;
padding: 30px 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
.fail-icon-container {
margin-bottom: 20px;
.fail-icon {
width: 120px;
height: 120px;
}
}
.fail-text {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.divider {
width: 100%;
height: 1px;
background-color: #eee;
margin-bottom: 20px;
}
.tip-text {
font-size: 16px;
color: #999;
margin-bottom: 15px;
}
.finger-icon {
width: 60px;
height: 60px;
margin-bottom: 15px;
image {
width: 100%;
height: 100%;
}
}
.action-btn {
width: 200px;
height: 45px;
line-height: 45px;
text-align: center;
border-radius: 22.5px;
border: 1px solid #2879ff;
color: #2879ff;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,345 @@
<template>
<view class="payment-page">
<!-- 倒计时区域 -->
<view class="countdown-section">
<view class="countdown-icon">
<u-icon name="clock" size="22" color="#fff"></u-icon>
</view>
<view class="countdown-text">待支付</view>
<view class="countdown-timer">
<text>剩余</text>
<text class="time-value">{{ countdownTime }}</text>
</view>
</view>
<view class="scrollable-content">
<!-- 学生信息 -->
<view class="student-info-card">
<view class="card-title">学生信息</view>
<view class="student-info">
<view class="student-name">{{ curXs.xm }}</view>
<view class="student-class">{{ curXs.bjmc }}</view>
</view>
</view>
<!-- 就餐标准信息 -->
<view class="jc-bz-info-card">
<view class="card-title">报名就餐标准</view>
<view class="jc-bz-list">
<view
v-for="(jcBz, index) in jcBzList"
:key="index"
class="jc-bz-item"
>
<view class="jc-bz-name">{{ jcBz.bzmc }}</view>
<view class="jc-bz-price">¥{{ jcBz.jfje }}</view>
</view>
</view>
</view>
</view>
<!-- 底部支付区域 -->
<view class="payment-footer">
<view class="total-amount">
<text>总金额</text>
<text class="amount-value">¥{{ totalJe }}</text>
</view>
<view class="action-buttons">
<view class="cancel-btn" @click="cancelRegistration">取消报名</view>
<view class="pay-btn" @click="payNow">立即支付</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jcCancelBmJcBzApi, jcFqJcBzJfjApi, jcGetJcBzPayExpiredTimeApi } from "@/api/base/jcApi";
const { getCurXs, getUser } = useUserStore();
const { getData, setData } = useDataStore();
//
const curXs = computed(() => getCurXs);
//
const jcBzList = computed(() => getData.jcBzList);
//
const totalJe = computed(() => {
// jcBzList.value
if (!jcBzList.value || !jcBzList.value.length) {
return 0;
}
let total = 0;
for (let i = 0; i < jcBzList.value.length; i++) {
total += jcBzList.value[i].jfje;
}
return total;
});
//
const countdownTime = ref("1分20秒");
let timer: any = null;
let seconds = 1 * 60 + 20; // 120
//
const startCountdown = () => {
timer = setInterval(() => {
seconds--;
if (seconds <= 0) {
clearInterval(timer);
uni.showModal({
title: "支付超时",
content: "支付已超时,请重新选课",
showCancel: false,
success: () => {
cancelRegistration();
},
});
return;
}
const minutes = Math.floor(seconds / 60);
const remainSeconds = seconds % 60;
countdownTime.value = `${minutes}${remainSeconds}`;
}, 1000);
};
//
const goBack = () => {
uni.reLaunch({
url: '/pages/base/home/index'
});
};
//
const cancelRegistration = () => {
uni.showModal({
title: "取消报名",
content: "确定要取消报名吗?",
success: async (res) => {
if (res.confirm) {
try {
await jcCancelBmJcBzApi({
xsId: getData.xsId,
jcBzIds: getData.jcBzIds
});
uni.showToast({
title: "已取消报名",
icon: "success",
});
goBack();
} catch (error) {
console.error('取消报名失败:', error);
uni.showToast({
title: "取消报名失败",
icon: "none",
});
}
}
},
});
};
//
const payNow = async () => {
try {
const res = await jcFqJcBzJfjApi({
xsId: getData.xsId,
jcBzIds: getData.jcBzIds,
jffs: "四川农信", // TODO:
jzId: getUser.jzId,
userId: getUser.userId,
openId: getUser.openId,
});
if (res.resultCode === 1 && res.result) {
setData({
...getData,
...res.result
});
uni.redirectTo({
url: `/pages/base/jc/pay/wait?payUrl=${encodeURIComponent(res.result.cashierPayHtml)}`
});
}
} catch (error) {
console.log(error);
uni.showToast({
title: "发起支付失败",
icon: "error",
});
}
};
onMounted(async() => {
try {
const res = await jcGetJcBzPayExpiredTimeApi({ xsId: getCurXs.id });
console.log('获取支付倒计时', res);
if (res.resultCode === 1) {
seconds = res.result;
startCountdown();
}
} catch (error) {
console.error('获取支付倒计时失败:', error);
}
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style lang="scss" scoped>
.payment-page {
min-height: 100%;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
//
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // iOS
padding: 15px;
}
.student-info-card,
.jc-bz-info-card {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
}
.student-info {
.student-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 5px;
}
.student-class {
font-size: 14px;
color: #666;
}
}
.jc-bz-list {
.jc-bz-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.jc-bz-name {
font-size: 14px;
color: #333;
}
.jc-bz-price {
font-size: 14px;
color: #ff6b00;
font-weight: bold;
}
}
}
.countdown-section {
display: flex;
align-items: center;
padding: 15px;
background-color: #2879ff;
.countdown-icon {
margin-right: 10px;
}
.countdown-text {
font-size: 16px;
font-weight: 500;
color: #fff;
margin-right: auto;
}
.countdown-timer {
font-size: 15px;
color: #fff;
.time-value {
color: #ff4d4f;
font-weight: 500;
}
}
}
.payment-footer {
position: sticky;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 15px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.total-amount {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 15px;
color: #333;
.amount-value {
color: #ff6b00;
font-size: 20px;
font-weight: 500;
}
}
.action-buttons {
display: flex;
justify-content: space-between;
.cancel-btn,
.pay-btn {
width: 48%;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 22px;
font-size: 16px;
}
.cancel-btn {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
.pay-btn {
background-color: #ff8c00;
color: #fff;
}
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<BasicLayout>
<view class="payment-success">
<!-- 成功提示卡片 -->
<view class="success-card">
<view class="success-icon-container">
<image
class="success-icon-bg"
src="/static/base/11223.png"
mode="aspectFit"
></image>
<view class="success-icon">
<u-icon name="checkmark" size="40" color="#fff"></u-icon>
</view>
</view>
<view class="success-text">恭喜你报名成功</view>
</view>
<!-- 这里显示学生信息和报名的就餐标准信息 -->
</view>
<template #bottom>
<!-- <view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="返回"
class="ml-15 mr-7"
:plain="true"
@click=""
/>
</view>
</view> -->
</template>
</BasicLayout>
</template>
<script setup lang="ts">
//
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
//
});
</script>
<style lang="scss" scoped>
.payment-success {
background-color: #f5f7fa;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
height: 44px;
background-color: #2879ff;
.nav-left {
width: 40px;
height: 40px;
display: flex;
align-items: center;
}
.nav-title {
font-size: 18px;
font-weight: 500;
color: #fff;
}
.nav-right {
width: 40px;
display: flex;
justify-content: flex-end;
}
}
.success-card {
margin: 15px;
background-color: #fff;
border-radius: 8px;
padding: 30px 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
.success-icon-container {
position: relative;
width: 120px;
height: 120px;
margin-bottom: 20px;
.success-icon-bg {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.success-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
background-color: #2879ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
.success-text {
font-size: 20px;
font-weight: bold;
color: #333;
}
}
</style>

View File

@ -0,0 +1,412 @@
<template>
<view class="payment-page">
<!-- 倒计时区域 -->
<view class="countdown-section">
<view class="countdown-icon">
<u-icon name="clock" size="22" color="#fff"></u-icon>
</view>
<view class="countdown-text">待支付</view>
<view class="countdown-timer">
<text>剩余</text>
<text class="time-value">{{ countdownTime }}</text>
</view>
</view>
<view class="scrollable-content">
<!-- H5环境下的处理 -->
<view v-if="isH5" class="h5-payment-container">
<view class="payment-info">
<view class="payment-title">支付信息</view>
<view class="payment-url">{{ payUrl }}</view>
</view>
<view class="payment-actions">
<button class="open-payment-btn" @click="openPaymentUrl">打开支付页面</button>
<button class="copy-url-btn" @click="copyPaymentUrl">复制支付链接</button>
</view>
<view class="payment-tips">
<text>提示</text>
<text>1. 点击"打开支付页面"按钮在新窗口打开支付</text>
<text>2. 支付完成后请返回此页面</text>
<text>3. 如无法打开请复制链接到浏览器中打开</text>
</view>
</view>
<!-- 非H5环境使用web-view -->
<web-view v-else :src="payUrl" @error="handleWebViewError" class="payment-webview"></web-view>
</view>
<!-- 底部支付区域 -->
<view class="payment-footer">
<view class="action-buttons">
<view class="cancel-btn" @click="cancelRegistration">取消报名</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
// import { jzGetQkExpiredTime, jzXkCancelApi, jzXkJfCxjApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
const { getCurXs, initWs, setWsCallback } = useUserStore();
const { getData } = useDataStore();
const payUrl = ref("");
const isH5 = ref(false);
// H5
const checkPlatform = () => {
// #ifdef H5
isH5.value = true;
// #endif
};
// URL
const openPaymentUrl = () => {
if (payUrl.value) {
//
window.open(payUrl.value, '_blank');
}
};
// URL
const copyPaymentUrl = () => {
if (payUrl.value) {
// #ifdef H5
if (navigator.clipboard) {
navigator.clipboard.writeText(payUrl.value).then(() => {
uni.showToast({
title: '链接已复制',
icon: 'success'
});
}).catch(() => {
//
const textArea = document.createElement('textarea');
textArea.value = payUrl.value;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
uni.showToast({
title: '链接已复制',
icon: 'success'
});
});
} else {
//
const textArea = document.createElement('textarea');
textArea.value = payUrl.value;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
uni.showToast({
title: '链接已复制',
icon: 'success'
});
}
// #endif
}
};
// web-view
const handleWebViewError = (e: any) => {
console.error('web-view加载错误:', e);
uni.showModal({
title: '加载失败',
content: '支付页面加载失败,请检查网络连接或联系客服',
showCancel: false
});
};
setWsCallback((type: string, res: any) => {
console.log('收到WebSocket消息:', type, res.data);
// data
const dataObj = JSON.parse(res.data);
if (dataObj.action === 'pay') {
uni.showToast({
title: '支付成功',
icon: 'success',
});
setWsCallback((type: string, res: any) => {})
//
setTimeout(() => {
uni.reLaunch({
url: "/pages/base/xk/pay/success",
});
}, 1000)
}
});
//
const countdownTime = ref("1分20秒");
let timer: any = null;
let seconds = 1 * 60 + 20; // 120
//
const startCountdown = () => {
timer = setInterval(() => {
seconds--;
if (seconds <= 0) {
clearInterval(timer);
uni.showModal({
title: "支付超时",
content: "支付已超时,请重新选课",
showCancel: false,
success: () => {
cancelRegistration();
},
});
return;
}
const minutes = Math.floor(seconds / 60);
const remainSeconds = seconds % 60;
countdownTime.value = `${minutes}${remainSeconds}`;
}, 1000);
};
//
const goBack = () => {
uni.reLaunch({
url: getData.backUrl
});
};
//
const cancelRegistration = () => {
// uni.showModal({
// title: "",
// content: "",
// success: async (res) => {
// if (res.confirm) {
// await jzXkCancelApi({
// xsId: getData.xsId,
// xkId: getData.xkId
// });
// uni.showToast({
// title: "",
// icon: "success",
// });
// goBack();
// }
// },
// });
};
onLoad(async (options: any) => {
//
checkPlatform();
if (options.payUrl) {
payUrl.value = decodeURIComponent(options.payUrl);
console.log('支付URL:', payUrl.value);
// const res = await jzGetQkExpiredTime({ xsId: getCurXs.id} );
// seconds = res.result;
// initWs();
// startCountdown();
} else {
uni.showToast({ title: '缺少支付地址', icon: 'none' })
setTimeout(() => {
goBack();
}, 1000)
}
openPaymentUrl();
// try {
// const res = await jzXkJfCxjApi({
// orderNumber: getData.orderNumber,
// xsId: getData.xsId,
// xkId: getData.xkId
// });
// // 01-02-03-04-
// const { orderStat, respCode } = res.result;
// if ("0000000000" === respCode) {
// if ("02" === orderStat) {
// uni.reLaunch({
// url: "/pages/base/xk/pay/success",
// });
// } else if ("03" === orderStat) {
// uni.reLaunch({
// url: "/pages/base/xk/pay/fail",
// });
// }
// } else {
// uni.showToast({
// title: "",
// icon: "error",
// });
// }
// } catch (error) {
// console.log("", error);
// }
});
onBeforeUnmount(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style lang="scss" scoped>
.payment-page {
min-height: 100vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
position: relative;
}
.scrollable-content {
flex: 1;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
.payment-webview {
width: 100% !important;
height: 100% !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
flex: 1;
min-height: 0;
}
// H5
.h5-payment-container {
padding: 20px;
background-color: #fff;
margin: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.payment-info {
margin-bottom: 20px;
.payment-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.payment-url {
font-size: 14px;
color: #666;
word-break: break-all;
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
}
}
.payment-actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
button {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
border: none;
cursor: pointer;
&.open-payment-btn {
background-color: #2879ff;
color: #fff;
}
&.copy-url-btn {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
}
}
.payment-tips {
background-color: #fff7e6;
border: 1px solid #ffd591;
border-radius: 4px;
padding: 15px;
text {
display: block;
font-size: 14px;
color: #d46b08;
line-height: 1.5;
margin-bottom: 5px;
&:first-child {
font-weight: bold;
margin-bottom: 8px;
}
}
}
.countdown-section {
display: flex;
align-items: center;
padding: 15px;
background-color: #2879ff;
flex-shrink: 0;
.countdown-icon {
margin-right: 10px;
}
.countdown-text {
font-size: 16px;
font-weight: 500;
color: #fff;
margin-right: auto;
}
.countdown-timer {
font-size: 15px;
color: #fff;
.time-value {
color: #ff4d4f;
font-weight: 500;
}
}
}
.payment-footer {
position: sticky;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 15px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
.action-buttons {
.cancel-btn {
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 22px;
font-size: 16px;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<view class="jc-record-page">
<!-- 记录信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<!-- 学生选择部分 -->
<XsPicker :is-bar="true" />
</view>
</view>
<!-- 可滚动的内容区域 -->
<view class="scrollable-content">
<JcRecordList :xs-id="curXs.id" />
</view>
</view>
</template>
<script setup lang="ts">
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import JcRecordList from "@/pages/base/components/JcRecordList/index.vue"
import { useUserStore } from "@/store/modules/user";
const { getCurXs } = useUserStore();
const curXs = computed(() => getCurXs);
//
onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.jc-record-page {
min-height: 100%;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
}
}
//
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // iOS
}
</style>

View File

@ -5,6 +5,7 @@ export const useDataStore = defineStore({
state: () => ({
data: {},
kcData: {},
jcBzData: {},
global: {},
file: {},
appCode: "JZ"
@ -22,6 +23,9 @@ export const useDataStore = defineStore({
getKcData(): any {
return this.kcData;
},
getJcBzData(): any {
return this.jcBzData;
},
getAppCode(): string {
return this.appCode;
},
@ -39,6 +43,9 @@ export const useDataStore = defineStore({
setKcData(data: any) {
this.kcData = data;
},
setJcBzData(data: any) {
this.jcBzData = data;
},
},
persist: {
enabled: true,