SAAS模式调整

This commit is contained in:
hebo 2026-04-07 20:03:50 +08:00
parent e7a726af56
commit 79ea4d33ce
5 changed files with 271 additions and 105 deletions

View File

@ -28,28 +28,39 @@
:class="{
'jc-bz-item-disabled': jcBz.paidStatus === 'paid'
}"
@click="handleJcBzClick(jcBz)"
>
<view class="jc-bz-info">
<view class="jc-bz-content">
<view class="jc-bz-name">{{ jcBz.bzMc || '暂无标准名称' }}</view>
<view class="jc-bz-price">
价格<text class="price-value">¥{{ jcBz.bzJe || 0 }}</text>
</view>
<view class="jc-bz-desc">{{ jcBz.bzSm || '暂无描述' }}</view>
<view class="jc-bz-row">
<view class="jc-bz-info" @click="handleJcBzClick(jcBz)">
<view class="jc-bz-content">
<view class="jc-bz-name">{{ jcBz.bzMc || '暂无标准名称' }}</view>
<view class="jc-bz-price">
价格<text class="price-value">¥{{ jcBz.bzJe || 0 }}</text>
</view>
<view class="jc-bz-desc">{{ jcBz.bzSm || '暂无描述' }}</view>
<!-- 显示更多详细信息 -->
<view class="jc-bz-details">
<view class="detail-item" v-if="jcBz.jfKsSj">
<text class="detail-label">缴费开始</text>
<text class="detail-value">{{ jcBz.jfKsSj }}</text>
</view>
<view class="detail-item" v-if="jcBz.jfJsSj">
<text class="detail-label">缴费结束</text>
<text class="detail-value">{{ jcBz.jfJsSj }}</text>
<!-- 显示更多详细信息 -->
<view class="jc-bz-details">
<view class="detail-item" v-if="jcBz.jfKsSj">
<text class="detail-label">缴费开始</text>
<text class="detail-value">{{ jcBz.jfKsSj }}</text>
</view>
<view class="detail-item" v-if="jcBz.jfJsSj">
<text class="detail-label">缴费结束</text>
<text class="detail-value">{{ jcBz.jfJsSj }}</text>
</view>
</view>
</view>
</view>
<view class="jc-bz-side" @click.stop>
<view
v-if="jcBz.paidStatus === 'unpaid' || jcBz.paidStatus === 'notApplied'"
class="pay-btn"
@click="handlePayClick(jcBz)"
>
点击缴费
</view>
<text v-else-if="jcBz.paidStatus === 'paid'" class="paid-tag">已缴费</text>
</view>
</view>
</view>
</view>
@ -59,7 +70,6 @@
<text>暂无就餐标准</text>
</view>
<BasicSign ref="signCompRef" title="签名"></BasicSign>
</view>
</view>
@ -73,14 +83,11 @@
<script setup lang="ts">
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import BasicSign from "@/components/BasicSign/Sign.vue"
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { checkXsJcBmApi, jcXsBmJcApi } from "@/api/base/jcApi";
import { JC_PAY_REDIRECT_URL } from "@/config";
const signCompRef = ref<any>(null);
const { getCurXs, getUser } = useUserStore();
const { getJc, setJc, setJcBz, setData } = useDataStore();
@ -152,23 +159,25 @@ const getJcBzList = async () => {
}
};
//
//
const handleJcBzClick = (jcBz: any) => {
//
if (jcBz.paidStatus === 'paid') {
uni.showToast({
title: '该标准已缴费,无法重复选择',
title: '已缴费成功!',
icon: 'none'
});
return;
}
handlePayClick(jcBz);
};
//
/** 右侧「点击缴费」:未报名先无签名报名再跳转支付;已报名未缴费直接跳转支付 */
const handlePayClick = (jcBz: any) => {
if (jcBz.paidStatus === 'paid') return;
if (jcBz.paidStatus === 'unpaid') {
//
navigateToPayment(jcBz);
} else if (jcBz.paidStatus === 'notApplied') {
goToQrCode(jcBz);
submitBmWithoutSign(jcBz);
}
};
@ -187,14 +196,8 @@ const navigateToPayment = (jcBz: any) => {
}
};
//
const goToQrCode = async (jcBz: any) => {
//
const data = await signCompRef.value.getSyncSignature();
if (!data) {
console.log("没有签名,或者取消了");
return;
}
//
const submitBmWithoutSign = async (jcBz: any) => {
uni.showLoading({
title: '报名中...'
});
@ -209,7 +212,7 @@ const goToQrCode = async (jcBz: any) => {
xm: curXs.value.xm,
bzId: jcBz.id,
jzId: getUser.jzId,
qmFile: data.base64 || '', //
qmFile: '',
bjId: curXs.value.bjId,
bjmc: curXs.value.bjmc
};
@ -223,21 +226,9 @@ const goToQrCode = async (jcBz: any) => {
icon: 'success'
});
}, 500);
//
jcBz.paidStatus = 'unpaid';
// store
setJcBz(jcBz);
//
const payUrl = JC_PAY_REDIRECT_URL;
try {
if (typeof (window as any).plus !== 'undefined' && (window as any).plus.runtime?.openURL) {
(window as any).plus.runtime.openURL(payUrl);
} else {
window.location.href = payUrl;
}
} catch (_e) {
window.location.href = payUrl;
}
navigateToPayment(jcBz);
} else {
uni.hideLoading();
setTimeout(() => {
@ -333,8 +324,16 @@ onMounted(() => {
opacity: 0.6;
}
.jc-bz-row {
display: flex;
flex-direction: row;
align-items: stretch;
}
.jc-bz-info {
display: flex;
flex: 1;
min-width: 0;
.jc-bz-content {
flex: 1;
@ -398,14 +397,32 @@ onMounted(() => {
}
}
}
}
.selection-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
margin-left: 10px;
}
.jc-bz-side {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding-left: 12px;
margin-left: 4px;
}
.pay-btn {
padding: 8px 14px;
background: linear-gradient(135deg, #ff8c42, #ff6b00);
color: #fff;
font-size: 13px;
font-weight: 500;
border-radius: 20px;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(255, 107, 0, 0.35);
}
.paid-tag {
font-size: 12px;
color: #67c23a;
white-space: nowrap;
}
}
}

View File

@ -100,28 +100,41 @@
<!-- 添加签名组件 - 只在需要时显示 -->
<BasicSign v-if="showSignature" ref="signCompRef" :title="signTitle"></BasicSign>
<template #bottom>
<view class="bottom-actions">
<button
v-if="!relaySuccess"
class="action-btn publish-btn"
@click="onRelayClick"
>
接龙
</button>
<button
v-else
class="action-btn return-btn"
@click="handleReturn"
>
返回
</button>
<view class="bottom-slot-wrap">
<transition name="finger-fade">
<view
v-if="!isLoading && !relaySuccess && showFingerHint"
class="finger-float"
aria-hidden="true"
>
<text class="finger-text">👆</text>
</view>
</transition>
<view class="bottom-bar">
<view class="bottom-actions">
<button
v-if="!relaySuccess"
class="action-btn publish-btn"
@click="onRelayClick"
>
点击接龙
</button>
<button
v-else
class="action-btn return-btn"
@click="handleReturn"
>
返回
</button>
</view>
</view>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick } from "vue";
import { ref, computed, watch, nextTick, onUnmounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getByJlIdApi, jlzxFindByJlParamsApi, relayFinishApi } from "@/api/base/server";
import { imagUrl } from "@/utils";
@ -133,7 +146,7 @@ const noticeId = ref<string>("");
const noticeDetail = ref<any>(null);
const isLoading = ref(false);
const studentList = ref<any[]>([]);
const descExpanded = ref(false);
const descExpanded = ref(true);
//
const njId = ref<string>("");
@ -148,6 +161,42 @@ const selectedOption = ref<string>("");
//
const relaySuccess = ref<boolean>(false);
/** 底部手指提示加载完成后浮动展示5 秒后淡出隐藏 */
const showFingerHint = ref(false);
const fingerDismissScheduled = ref(false);
let fingerHideTimer: ReturnType<typeof setTimeout> | null = null;
function clearFingerTimer() {
if (fingerHideTimer != null) {
clearTimeout(fingerHideTimer);
fingerHideTimer = null;
}
}
watch(isLoading, (loading) => {
if (loading) return;
if (relaySuccess.value) return;
if (fingerDismissScheduled.value) return;
fingerDismissScheduled.value = true;
showFingerHint.value = true;
clearFingerTimer();
fingerHideTimer = setTimeout(() => {
showFingerHint.value = false;
fingerHideTimer = null;
}, 5000);
});
onUnmounted(() => {
clearFingerTimer();
});
watch(relaySuccess, (ok) => {
if (ok) {
clearFingerTimer();
showFingerHint.value = false;
}
});
//
const signCompRef = ref<any>(null);
const signTitle = ref<string>("签名");
@ -158,39 +207,52 @@ const showSignature = ref<boolean>(false);
const userStore = useUserStore();
const currentStudent = computed(() => userStore.curXs);
const descPreview = computed(() => {
if (!noticeDetail.value?.jlms) return '';
// 使 HTML
const text = noticeDetail.value.jlms
.replace(/<[^>]+>/g, '') // HTML
.replace(/&nbsp;/g, '\u3000') //
/** 将常见 HTML 实体转为字符;小程序 rich-text 对 &mdash; 等命名实体支持不完整,需先解码 */
function decodeHtmlEntitiesForRichText(html: string): string {
if (!html) return '';
return html
.replace(/&nbsp;/g, '\u00A0')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&ldquo;/g, '"') //
.replace(/&rdquo;/g, '"') //
.replace(/&lsquo;/g, "'") //
.replace(/&rsquo;/g, "'") //
.replace(/&mdash;/g, '—') //
.replace(/&ndash;/g, '') //
.replace(/&hellip;/g, '…') //
.replace(/&middot;/g, '·') //
.replace(/&#\d+;/g, '') // HTML
.replace(/[\r\n]+/g, ' ') //
.replace(/[ \t]+/g, ' ') //
.trim();
.replace(/&ldquo;/g, '\u201c')
.replace(/&rdquo;/g, '\u201d')
.replace(/&lsquo;/g, '\u2018')
.replace(/&rsquo;/g, '\u2019')
.replace(/&mdash;/g, '\u2014')
.replace(/&ndash;/g, '\u2013')
.replace(/&hellip;/g, '\u2026')
.replace(/&middot;/g, '\u00b7')
.replace(/&#(\d+);/g, (_, n) => {
const code = parseInt(n, 10);
return Number.isFinite(code) ? String.fromCharCode(code) : _;
})
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => {
const code = parseInt(h, 16);
return Number.isFinite(code) ? String.fromCharCode(code) : _;
})
.replace(/&amp;/g, '&');
}
const descPreview = computed(() => {
if (!noticeDetail.value?.jlms) return '';
const stripped = String(noticeDetail.value.jlms).replace(/<[^>]+>/g, '');
const decoded = decodeHtmlEntitiesForRichText(stripped);
const text = decoded
.replace(/\u00A0/g, ' ')
.replace(/[\r\n]+/g, ' ')
.replace(/[ \t]+/g, ' ')
.trim();
return text.slice(0, 100) + (text.length > 100 ? '...' : '');
});
// CSS text-indent
const processedContent = computed(() => {
if (!noticeDetail.value?.jlms) return '';
// 4 &nbsp; CSS 2em
return noticeDetail.value.jlms
.replace(/(&nbsp;\s*){4}/g, '') // 4 &nbsp;
.replace(/(&nbsp;){4}/g, ''); // 4 &nbsp;
const stripped = String(noticeDetail.value.jlms)
.replace(/(&nbsp;\s*){4}/g, '')
.replace(/(&nbsp;){4}/g, '');
return decodeHtmlEntitiesForRichText(stripped);
});
//
@ -518,18 +580,72 @@ watch(studentList, (list) => {
<style scoped lang="scss">
.notice-detail-page {
background-color: #f4f5f7;
padding-bottom: 70px;
padding-bottom: 80px;
padding: 15px;
box-sizing: border-box;
}
.bottom-slot-wrap {
position: relative;
}
/* 浮动在底部按钮上方,不占布局高度 */
.finger-float {
position: fixed;
left: 0;
right: 0;
bottom: calc(56px + constant(safe-area-inset-bottom));
bottom: calc(56px + env(safe-area-inset-bottom));
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 998;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.12));
}
.finger-text {
font-size: 40px;
line-height: 1;
display: inline-block;
transform-origin: center center;
animation: fingerFloatHint 1.25s ease-in-out infinite;
}
/* 手指旋转 180°并做轻微上下浮动 */
@keyframes fingerFloatHint {
0%,
100% {
transform: translateY(0) rotate(180deg) scale(1);
}
50% {
transform: translateY(-12px) rotate(180deg) scale(1.06);
}
}
.finger-fade-enter-active,
.finger-fade-leave-active {
transition: opacity 0.45s ease;
}
.finger-fade-enter-from,
.finger-fade-leave-to {
opacity: 0;
}
.bottom-bar {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
}
.bottom-actions {
display: flex;
justify-content: space-around;
align-items: center;
padding: 12px 15px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
.action-btn {
width: 90%;

View File

@ -34,6 +34,10 @@
<text class="label">银行账号:</text>
<text class="value">{{ xkTf.khZh }}</text>
</view>
<view class="info-row">
<text class="label">开户网点:</text>
<text class="value">{{ xkTf.khWd || '—' }}</text>
</view>
<view class="info-column">
<text class="label">缴费凭证:</text>
<text class="value">

View File

@ -94,6 +94,15 @@ const [register, { getValue, setValue }] = useForm({
required: true,
componentProps: { },
},
{
field: "khWd",
label: "开户网点",
component: "BasicInput",
required: true,
componentProps: {
placeholder: "如:泸州江阳支行",
},
},
{
field: "jfPz",
label: "缴费凭证",

View File

@ -113,7 +113,7 @@ interface TaskItem {
kcmc?: string;
kcId?: string;
xsId?: string;
ispj?: string; // A:
ispj?: string; // A: B:
}
const taskList = ref<TaskItem[]>([]);
@ -167,7 +167,11 @@ onLoad(async (options) => {
xsId.value = options.xsId;
}
loadTaskList();
// openId onShow isLoginReady false onLoad
// onShow onLoad
if (options && options.openId) {
loadTaskList();
}
});
onShow(() => {
@ -231,11 +235,27 @@ const loadTaskList = async () => {
}
};
/** 该任务是否已被教师评价(锁定独立项目选择) */
const isTaskEvaluated = (t: TaskItem) =>
t.zpzt === 'C' || t.ispj === 'A';
// -
const startChallenge = (task: TaskItem) => {
//
const submittedTask = taskList.value.find(t => t.zpzt === 'A' && t.id !== task.id);
if (submittedTask) {
//
if (isTaskEvaluated(task)) {
uni.showToast({
title: '该任务已评价,无法再次挑战',
icon: 'none',
duration: 2000
});
return;
}
// ispj=B
const otherLocked = taskList.value.find(
(t) => t.id !== task.id && isTaskEvaluated(t)
);
if (otherLocked) {
uni.showToast({
title: '已完成挑战',
icon: 'none',