Compare commits

...

2 Commits

Author SHA1 Message Date
8bdde206b0 新苗调整 2026-05-26 15:17:36 +08:00
dda9a21de3 SAAS模式调整 2026-05-11 15:52:59 +08:00
11 changed files with 976 additions and 300 deletions

View File

@ -68,6 +68,12 @@ export const xsKsccApi = async (params: any) => {
return await get("/mobile/jz/kscc", params);
};
/**
* - bjId yfzc_kscc.bj_id
*/
export const ksccFindAllXqKsccTreeApi = (params?: { bjId?: string }) =>
get("/api/kscc/findAllXqKsccTree", params ?? {});
/**
*
*/

View File

@ -46,6 +46,20 @@ export const questionnaireAnswerFindByUserApi = async (params: { questionnaireId
return await get("/api/questionnaireAnswer/findByUser", params);
};
/**
*
*/
export const questionnaireAnswerFindByUserOrFamilyApi = async (params: {
questionnaireId: string;
xxtsId?: string;
xsId?: string;
}) => {
const q: Record<string, string> = { questionnaireId: params.questionnaireId };
if (params.xxtsId) q.xxtsId = params.xxtsId;
if (params.xsId) q.xsId = params.xsId;
return await get("/api/questionnaireAnswer/findByUserOrFamily", q);
};
/**
*
*/

View File

@ -175,6 +175,13 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/xszp/resource-detail",
"style": {
"navigationBarTitleText": "资源详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/jl/detailwb",
"style": {
@ -220,8 +227,8 @@
{
"path": "pages/base/grades/list",
"style": {
"navigationBarTitleText": "考试列表",
"enablePullDownRefresh": false
"navigationBarTitleText": "成绩查询",
"enablePullDownRefresh": true
}
},
{

View File

@ -20,7 +20,7 @@
<view class="score-info-section">
<view class="score-left">
<text class="total-score">{{ ksccKmList.length }}</text>
<text class="full-score">满分{{ totalKmFs }}</text>
<text v-if="showXsFs" class="full-score">满分{{ totalKmFs }}</text>
</view>
<text class="grade">{{ curKsdj.dj || '-' }}</text>
</view>
@ -59,12 +59,19 @@
>
<view class="subject-header">
<text class="subject-name">{{ kscj.km?.kmmc || kscj.kmmc || '未知科目' }}</text>
<text class="detail-btn" :style="{ color: kscj.djclr || '#909399' }" v-if="kscj.dj">{{ kscj.djBx }}</text>
<!-- sf_xs_fs=0隐藏右侧等级表现文案如优秀左侧字母等级 dj 仍展示 -->
<text
class="detail-btn"
:style="{ color: kscj.djclr || '#909399' }"
v-if="showXsFs && kscj.djBx"
>
{{ kscj.djBx }}
</text>
</view>
<view class="subject-body">
<text class="subject-grade" :style="{ color: kscj.djclr || '#303133' }">{{ kscj.dj || '-' }}</text>
<view class="grade-info-wrapper">
<text v-if="kscc.sfXsFs && kscj.ksfs" class="subject-score">{{ kscj.ksfs }}</text>
<view class="grade-info-wrapper" v-if="showXsFs">
<text v-if="showSubjectNumericScore(kscj)" class="subject-score">{{ kscj.ksfs }}</text>
</view>
</view>
</view>
@ -103,6 +110,7 @@
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import uCharts from "@/components/charts/u-charts.js";
import dayjs from "dayjs";
import { xsKscjApi, ksccPjFindByKsccAndXsApi } from "@/api/base/server";
@ -119,6 +127,42 @@ const fsScale = 100;
const curXs = ref<any>({})
const kscc = ref<any>({})
/** 从列表 URL 带入场次,避免依赖全局 store */
const ksccFromRoute = ref(false)
/** 与 yfzc_kscc.sf_xs_fs 一致:接口 showFs=false 时不展示卷面分及科目右侧等级表现文案djBx各科字母等级 dj 与顶部总等级仍展示 */
const showXsFs = ref(true);
function showSubjectNumericScore(kscj: any): boolean {
if (!showXsFs.value) return false;
if (kscj == null || kscj.ksfs === undefined || kscj.ksfs === null || kscj.ksfs === "") return false;
return true;
}
onLoad((options?: Record<string, string>) => {
const q = options || {};
const kid = q.ksccId != null ? String(q.ksccId).trim() : "";
if (!kid) return;
ksccFromRoute.value = true;
let name = "";
try {
name = q.ksmc != null ? decodeURIComponent(String(q.ksmc)) : "";
} catch {
name = q.ksmc != null ? String(q.ksmc) : "";
}
kscc.value = {
id: kid,
ksmc: name || "考试场次",
};
if (q.njmcId != null && String(q.njmcId).trim()) {
try {
kscc.value.njmcId = decodeURIComponent(String(q.njmcId));
} catch {
kscc.value.njmcId = String(q.njmcId);
}
}
})
const kscjList = ref<any>([])
const curKsdj = ref<any>({})
@ -234,76 +278,168 @@ const floatToInt = (floatNum: number, scale: number) => {
return Math.round(floatNum * scale);
}
let singleFlag = false;
/** 与后端 KsccKmCjServiceImpl.matchRule 一致initKsdj 后边界已为 scaled int */
function matchDjRule(score: number, boundary: unknown, rule: string | undefined): boolean {
if (boundary === null || boundary === undefined || boundary === "") return true;
const b = typeof boundary === "number" ? boundary : parseFloat(String(boundary));
if (Number.isNaN(b)) return true;
const r = rule != null ? String(rule).trim() : "";
if (!r) return true;
switch (r) {
case ">=":
return score >= b;
case ">":
return score > b;
case "<=":
return score <= b;
case "<":
return score < b;
default:
return true;
}
}
//
/** initKsdj 已将 kmDjList 的 zdf/zgf 转为与分数相同的 scale此处用 scaled 分数匹配 */
function resolveKmDjScaled(fs: number | null | undefined, kmId: string): any | null {
if (fs === null || fs === undefined || Number.isNaN(Number(fs))) return null;
const scoreScaled = floatToInt(Number(fs), fsScale);
const listDj = kmDjList.value.filter((d: any) => String(d.kmId || "") === String(kmId));
const hit = listDj.find((item: any) => {
const zdfRule =
item.zdfRule != null && String(item.zdfRule).trim() !== ""
? String(item.zdfRule).trim()
: ">=";
const zgfRule =
item.zgfRule != null && String(item.zgfRule).trim() !== ""
? String(item.zgfRule).trim()
: "<=";
return (
matchDjRule(scoreScaled, item.zdf, zdfRule) && matchDjRule(scoreScaled, item.zgf, zgfRule)
);
});
return hit || null;
}
/** 多场等级中取 sort 最优(数值越小越靠前)的一条 */
function pickBestDjMatch(rows: any[], scoreScaled: number): any | null {
const matches = rows.filter((item: any) => {
const zdfRule =
item.zdfRule != null && String(item.zdfRule).trim() !== ""
? String(item.zdfRule).trim()
: ">=";
const zgfRule =
item.zgfRule != null && String(item.zgfRule).trim() !== ""
? String(item.zgfRule).trim()
: "<=";
return (
matchDjRule(scoreScaled, item.zdf, zdfRule) && matchDjRule(scoreScaled, item.zgf, zgfRule)
);
});
if (!matches.length) return null;
return matches.reduce((best, cur) =>
Number(cur.sort ?? 9999) < Number(best.sort ?? 9999) ? cur : best
);
}
/**
* 场次总等级lx2先将本场得分之和线性映射到教务 djList 的刻度上界各档 zgf 最大值
* 再按 zdfRule/zgfRule 匹配当场次等级按模板如满分 400配置而本场科目满分之和为 200
* 避免误判刻度一致时不缩放 zhxy-jsd kspj/detail.vue 一致
*/
function resolveOverallKsDj(sumScaled: number, examFullMarksScaled: number): any {
const ksDjRows = djList.value.filter((d: any) => String(d.lx || "") !== "2");
if (!ksDjRows.length) return {};
let scoreForMatch = sumScaled;
if (examFullMarksScaled > 0) {
const maxZgfScaled = Math.max(0, ...ksDjRows.map((d: any) => Number(d.zgf) || 0));
if (maxZgfScaled > 0 && maxZgfScaled !== examFullMarksScaled) {
scoreForMatch = Math.round((sumScaled * maxZgfScaled) / examFullMarksScaled);
}
}
return pickBestDjMatch(ksDjRows, scoreForMatch) || {};
}
// kspj/detail
const initKsdj = () => {
let maxDj = 0.0;
let maxKmDj = 0.0;
djList.value.forEach((ksdj: any) => {
ksdj.zdf = floatToInt(ksdj.zdf, fsScale); //
ksdj.zgf = floatToInt(ksdj.zgf, fsScale); //
ksdj.djclr = colorMap[ksdj.dj] || ''; //
maxDj = Math.max(maxDj, ksdj.zgf);
ksdj.zdf = floatToInt(ksdj.zdf, fsScale);
ksdj.zgf = floatToInt(ksdj.zgf, fsScale);
ksdj.djclr = colorMap[ksdj.dj] || "";
});
kmDjList.value.forEach((kmdj: any) => {
kmdj.zdf = floatToInt(kmdj.zdf, fsScale); //
kmdj.zgf = floatToInt(kmdj.zgf, fsScale); //
kmdj.djclr = colorMap[kmdj.dj] || ''; //
maxKmDj = Math.max(maxKmDj, kmdj.zgf);
kmdj.zdf = floatToInt(kmdj.zdf, fsScale);
kmdj.zgf = floatToInt(kmdj.zgf, fsScale);
kmdj.djclr = colorMap[kmdj.dj] || "";
});
singleFlag = maxDj < maxKmDj + 100;
};
const rebuildData = () => {
let ksfsList: any[] = [];
let totalFs = 0.00;
let totalFs = 0.0;
totalKmFs.value = 0;
ksccKscjList.value = ksccKscjList.value.map((cj: any) => {
ksfsList.push(cj.ksfs);
totalFs += cj.ksfs;
let fs = floatToInt(cj.ksfs, fsScale);
cj.djclr = colorMap[cj.dj] || '';
//
totalFs += Number(cj.ksfs) || 0;
const djMissing = !cj.dj || String(cj.dj).trim() === "";
if (djMissing && kmDjList.value.length > 0 && cj.kmId) {
const resolved = resolveKmDjScaled(cj.ksfs, cj.kmId);
if (resolved) {
cj.dj = resolved.dj;
cj.djBx = resolved.djBx;
cj.djLx = resolved.djLx;
}
}
cj.djclr = colorMap[cj.dj] || "";
cj.km = ksccKmList.value.find((item: any) => item.kmId === cj.kmId) || {};
return cj;
});
//
radarData.categories = ksccKmList.value.map((item: any) => {
totalKmFs.value += item.kmfs;
totalKmFs.value += Number(item.kmfs) || 0;
return item.kmmc;
});
radarData.series = [{ name: "分数", data: ksfsList }];
//
totalFs = floatToInt(totalFs, fsScale);
if (singleFlag) {
totalFs = totalFs / ksccKmList.value.length;
}
curKsdj.value = djList.value.find((item: any) => item.zdf <= totalFs && item.zgf >= totalFs) || {};
}
const sumScaled = floatToInt(totalFs, fsScale);
const examFullMarksScaled = floatToInt(totalKmFs.value, fsScale);
curKsdj.value = resolveOverallKsDj(sumScaled, examFullMarksScaled);
};
onMounted(async () => {
pageLoading.value = true;
try {
curXs.value = getCurXs;
kscc.value = getData;
if (!ksccFromRoute.value) {
kscc.value = getData || {};
}
const ksccId = kscc.value?.id;
if (!ksccId) {
uni.showToast({ title: "请先选择考试场次", icon: "none" });
return;
}
const res = await xsKscjApi({
xsId: getCurXs.id,
ksccId: getData.id,
njmcId: getData.njmcId
ksccId,
njmcId: kscc.value?.njmcId ?? "",
});
if (res.resultCode == 1) {
sysKmList.value = res.result.kmList || [];
ksccKmList.value = res.result.ksccKmList || [];
kmDjList.value = res.result.kmDjList || [];
djList.value = res.result.djList || [];
ksccKscjList.value = res.result.ksccKscjList || [];
const r = res.result || {};
showXsFs.value = r.showFs !== false;
sysKmList.value = r.kmList || [];
ksccKmList.value = r.ksccKmList || [];
kmDjList.value = r.kmDjList || [];
djList.value = r.djList || [];
ksccKscjList.value = r.ksccKscjList || [];
initKsdj();
rebuildData();
}
//
try {
const pjRes = await ksccPjFindByKsccAndXsApi({
ksccId: getData.id,
ksccId,
xsId: getCurXs.id,
});
if (pjRes && pjRes.resultCode === 1 && pjRes.result && pjRes.result.pjBzr) {

View File

@ -1,132 +1,157 @@
<template>
<view class="ks-application-page">
<BasicListLayout @register="register">
<template v-slot="{ data }">
<view class="ks-card" @click="viewDetails(data)">
<view class="card-header">
<text class="applicant-name"
>{{ data.ksmc }}</text
<view class="page">
<view class="head-tip">请选择考试场次</view>
<scroll-view scroll-y class="scroll">
<view v-if="loading" class="state">加载中...</view>
<view v-else-if="!treeData.length" class="state">暂无考试场次数据</view>
<view v-else class="sections">
<view v-for="xq in treeData" :key="xqKey(xq)" class="section">
<text class="section-title">{{ xq.title || xq.label || "学期" }}</text>
<view
v-for="c in xq.children || []"
:key="ksccKey(c)"
class="kscc-row"
@click="goDetail(xq, c)"
>
<text class="kscc-name">{{ c.title || c.ksmc || "—" }}</text>
<text class="arrow"></text>
</view>
<view class="card-body">
<view class="info-row">
<text class="label">考试科目:</text>
<text class="value">{{ data.kmmc }}</text>
</view>
<view class="info-row">
<text class="label">开始时间:</text>
<text class="value">{{ data.kskstime }}</text>
</view>
<view class="info-row">
<text class="label">结束时间:</text>
<text class="value">{{ data.ksjstime }}</text>
<view v-if="!(xq.children || []).length" class="kscc-empty">该学期下暂无场次</view>
</view>
</view>
<view class="card-footer">
<text>查看详情</text>
<text class="arrow">
<uni-icons type="arrowright" size="16" color="#ccc"></uni-icons>
</text>
</view>
</view>
</template>
</BasicListLayout>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { xsKsccApi } from "@/api/base/server";
import { ref, onMounted } from "vue";
import { onPullDownRefresh } from "@dcloudio/uni-app";
import { ksccFindAllXqKsccTreeApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
const { getCurXs } = useUserStore();
const { setData } = useDataStore();
let pageParams = ref({
rows: 10,
xsId: getCurXs.id
})
const loading = ref(true);
const treeData = ref<any[]>([]);
const [register, { reload }] = useLayout({
api: xsKsccApi,
componentProps: {},
param: pageParams.value
function xqKey(xq: any) {
return String(xq.key ?? xq.id ?? xq.title ?? "");
}
function ksccKey(c: any) {
return String(c.key ?? c.id ?? c.title ?? "");
}
async function loadTree() {
loading.value = true;
try {
const xs = getCurXs as any;
const bjId = xs?.bjId ?? xs?.bj_id;
const res: any = await ksccFindAllXqKsccTreeApi(
bjId ? { bjId: String(bjId).trim() } : {}
);
const raw = res?.result ?? res?.rows ?? res?.data ?? res;
treeData.value = Array.isArray(raw) ? raw : [];
} catch {
treeData.value = [];
} finally {
loading.value = false;
}
}
function goDetail(xq: any, c: any) {
const ksccId = c?.key ?? c?.id;
if (!ksccId) {
uni.showToast({ title: "场次ID无效", icon: "none" });
return;
}
const xqTitle = String(xq?.title ?? xq?.label ?? "").trim();
const ksTitle = String(c?.title ?? c?.ksmc ?? "").trim();
const idEnc = encodeURIComponent(String(ksccId));
const ksEnc = encodeURIComponent(ksTitle || "考试场次");
uni.navigateTo({
url: `/pages/base/grades/detail?ksccId=${idEnc}&ksmc=${ksEnc}`,
});
}
onMounted(() => {
void loadTree();
});
//
const viewDetails = (item: any | null) => {
setData(item);
uni.navigateTo({ url: '/pages/base/grades/detail' });
};
onPullDownRefresh(() => {
void loadTree().finally(() => uni.stopPullDownRefresh());
});
</script>
<style lang="scss" scoped>
.ks-card {
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 15px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-header {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
.applicant-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
.card-body {
padding: 15px;
.info-row {
.page {
min-height: 100vh;
height: 100vh;
display: flex;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
flex-direction: column;
background: #f1f5f9;
box-sizing: border-box;
}
.label {
font-size: 14px;
color: #666;
width: 70px;
.head-tip {
padding: 20rpx 24rpx 12rpx;
font-size: 26rpx;
color: #64748b;
background: #f8fafc;
border-bottom: 1rpx solid #e2e8f0;
flex-shrink: 0;
margin-right: 8px;
}
.value {
font-size: 14px;
color: #333;
.scroll {
flex: 1;
min-height: 0;
height: 0;
box-sizing: border-box;
}
.sections {
padding: 16rpx 24rpx 40rpx;
}
.section {
margin-bottom: 28rpx;
}
.card-footer {
padding: 12px 15px;
border-top: 1px solid #f0f0f0;
.section-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #334155;
margin-bottom: 12rpx;
padding-left: 4rpx;
}
.kscc-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #888;
cursor: pointer;
justify-content: space-between;
padding: 28rpx 24rpx;
margin-bottom: 12rpx;
background: #fff;
border-radius: 16rpx;
border: 1rpx solid #e2e8f0;
box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.04);
}
.kscc-name {
flex: 1;
min-width: 0;
font-size: 30rpx;
color: #0f172a;
padding-right: 16rpx;
}
.arrow {
font-size: 16px;
color: #ccc;
font-size: 36rpx;
color: #94a3b8;
font-weight: 300;
}
.kscc-empty {
font-size: 26rpx;
color: #94a3b8;
padding: 16rpx 8rpx;
}
// ( BasicListLayout bottom )
.button {
padding: 10px 15px;
background-color: #447ade;
color: white;
.state {
text-align: center;
border-radius: 5px;
margin: 10px 15px; //
font-size: 16px;
color: #64748b;
padding: 80rpx 24rpx;
font-size: 28rpx;
}
</style>

View File

@ -118,7 +118,15 @@ import { ref, computed, watch, nextTick } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getByJlIdApi, jlzxFindByJlParamsApi, relayFinishApi } from "@/api/base/server";
import { imagUrl } from "@/utils";
import { BASE_IMAGE_URL } from "@/config";
import {
canPreview,
isImage,
isVideo,
previewFile,
previewImage,
previewVideo,
downloadFile,
} from "@/utils/filePreview";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading } from "@/utils/uniapp";
@ -395,59 +403,33 @@ onLoad(async (options) => {
}
});
//
// zhxy-jsd indexList / / kkFileView
const previewAttachment = (filePath: string) => {
if (!filePath) {
uni.showToast({ title: "附件链接无效", icon: "none" });
return;
}
const fileName = getFileName(filePath);
const ext = fileName.includes(".") ? (fileName.split(".").pop() || "").toLowerCase() : "";
const fullUrl = imagUrl(filePath);
// URL
const baseUrl = BASE_IMAGE_URL; // 使URL
const fullUrl = filePath.startsWith('http') ? filePath : baseUrl + filePath;
//
const fileExtension = filePath.split('.').pop()?.toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension || '');
//
if (isImage) {
uni.previewImage({
urls: [fullUrl],
current: fullUrl
if (isVideo(ext)) {
const title = fileName.replace(/\.[^.]+$/i, "") || fileName;
previewVideo(fullUrl, title);
return;
}
if (isImage(ext)) {
previewImage(fullUrl);
return;
}
if (canPreview(fileName)) {
previewFile(fullUrl, fileName, ext).catch(() => {
downloadFile(fullUrl, fileName);
});
return;
}
uni.showLoading({ title: "正在准备附件..." });
uni.downloadFile({
url: fullUrl,
success: (res) => {
if (res.statusCode === 200) {
const tempFilePath = res.tempFilePath;
uni.hideLoading();
// Attempt to open the downloaded file
uni.openDocument({
filePath: tempFilePath,
success: function (res) {
//
},
fail: function (err) {
uni.showToast({ title: "无法打开该文件类型", icon: "none" });
uni.hideLoading(); // Ensure loading is hidden on failure too
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
},
});
uni.showToast({ title: "该文件格式暂不支持预览", icon: "none" });
downloadFile(fullUrl, fileName);
};
watch(noticeDetail, (val) => {

View File

@ -138,7 +138,15 @@ 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";
import { BASE_IMAGE_URL } from "@/config";
import {
canPreview,
isImage,
isVideo,
previewFile,
previewImage,
previewVideo,
downloadFile,
} from "@/utils/filePreview";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading } from "@/utils/uniapp";
@ -503,59 +511,33 @@ onLoad(async (options) => {
}
});
//
// zhxy-jsd indexList / / kkFileView
const previewAttachment = (filePath: string) => {
if (!filePath) {
uni.showToast({ title: "附件链接无效", icon: "none" });
return;
}
const fileName = getFileName(filePath);
const ext = fileName.includes(".") ? (fileName.split(".").pop() || "").toLowerCase() : "";
const fullUrl = imagUrl(filePath);
// URL
const baseUrl = BASE_IMAGE_URL; // 使URL
const fullUrl = filePath.startsWith('http') ? filePath : baseUrl + filePath;
//
const fileExtension = filePath.split('.').pop()?.toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension || '');
//
if (isImage) {
uni.previewImage({
urls: [fullUrl],
current: fullUrl
if (isVideo(ext)) {
const title = fileName.replace(/\.[^.]+$/i, "") || fileName;
previewVideo(fullUrl, title);
return;
}
if (isImage(ext)) {
previewImage(fullUrl);
return;
}
if (canPreview(fileName)) {
previewFile(fullUrl, fileName, ext).catch(() => {
downloadFile(fullUrl, fileName);
});
return;
}
uni.showLoading({ title: "正在准备附件..." });
uni.downloadFile({
url: fullUrl,
success: (res) => {
if (res.statusCode === 200) {
const tempFilePath = res.tempFilePath;
uni.hideLoading();
// Attempt to open the downloaded file
uni.openDocument({
filePath: tempFilePath,
success: function (res) {
//
},
fail: function (err) {
uni.showToast({ title: "无法打开该文件类型", icon: "none" });
uni.hideLoading(); // Ensure loading is hidden on failure too
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
},
});
uni.showToast({ title: "该文件格式暂不支持预览", icon: "none" });
downloadFile(fullUrl, fileName);
};
watch(noticeDetail, (val) => {

View File

@ -353,7 +353,7 @@ import {
questionnaireCheckPermissionApi,
questionnaireQuestionFindPageApi,
questionnaireAnswerSaveApi,
questionnaireAnswerFindByUserApi,
questionnaireAnswerFindByUserOrFamilyApi,
questionnaireAnswerFindDetailApi
} from '@/api/base/wjApi';
import { useUserStore } from '@/store/modules/user';
@ -368,6 +368,7 @@ const options = ref<any>({});
const questionnaireId = ref('');
const openId = ref<string>(""); // openId
const xxtsId = ref<string>(""); // IDURLid
const xsId = ref<string>(""); // ID xsId
const loading = ref(true);
const submitting = ref(false); //
const currentStep = ref<'fill' | 'success' | 'noPermission' | 'alreadyFilled' | 'timeInvalid'>('fill');
@ -415,6 +416,7 @@ onLoad(async (params) => {
options.value = params;
openId.value = params?.openId || '';
xxtsId.value = params?.id || ''; // URLidxxtsId
xsId.value = (params?.xsId as string) || '';
// openId
if (openId.value) {
@ -500,8 +502,10 @@ const initializePage = async () => {
// 4.
if (!allowResubmit.value) {
const answerResult = await questionnaireAnswerFindByUserApi({
questionnaireId: questionnaireId.value
const answerResult = await questionnaireAnswerFindByUserOrFamilyApi({
questionnaireId: questionnaireId.value,
xxtsId: xxtsId.value || undefined,
xsId: xsId.value || undefined,
});
if (answerResult && answerResult.resultCode === 1) {
const hasAnswered = answerResult.result || answerResult.data;
@ -1091,6 +1095,9 @@ const doSubmitQuestionnaire = async () => {
if (xxtsId.value) {
submitParams.xxtsId = xxtsId.value;
}
if (xsId.value) {
submitParams.xsId = xsId.value;
}
//
const result = await questionnaireAnswerSaveApi(submitParams);

View File

@ -14,24 +14,27 @@
<view class="page-banner">
<view class="banner-content">
<view class="banner-title-wrapper">
<text class="banner-icon">📝</text>
<text class="banner-icon">{{ activeTab === 0 ? '📝' : '📚' }}</text>
<view class="banner-text">
<text class="banner-title">作品任务</text>
<text class="banner-subtitle">独立项目选择 · 学习成长</text>
<text class="banner-title">{{ activeTab === 0 ? '作品任务' : '作品资源' }}</text>
<text class="banner-subtitle">{{
activeTab === 0 ? '独立项目选择 · 学习成长' : '课程资料 · 在线查看'
}}</text>
</view>
</view>
<view class="task-count">
<text class="count-number">{{ taskList.length }}</text>
<text class="count-label">个任务</text>
<text class="count-number">{{ activeTab === 0 ? submitTasks.length : resourceTasks.length }}</text>
<text class="count-label">{{ activeTab === 0 ? '个任务' : '项资源' }}</text>
</view>
</view>
</view>
<!-- 独立项目选择提示 -->
<view class="selection-tip" v-if="taskList.length > 0">
<view class="tip-content">
<text class="tip-title">独立项目选择</text>
<text class="tip-desc">请从下方{{ taskList.length }}个任务中选择1个完成可自主挑战更高层级</text>
<view class="tab-bar">
<view class="tab-item" :class="{ active: activeTab === 0 }" @click="activeTab = 0">
<text class="tab-text">作品任务</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 1 }" @click="activeTab = 1">
<text class="tab-text">作品资源</text>
</view>
</view>
@ -42,16 +45,18 @@
<text class="loading-text">加载中...</text>
</view>
<template v-else-if="taskList.length > 0">
<template v-else-if="displayList.length > 0">
<view
v-for="task in taskList"
v-for="task in displayList"
:key="task.id"
class="task-card"
:class="{ 'task-card--resource': activeTab === 1 }"
@click="onTaskCardClick(task)"
>
<view class="card-header">
<view class="header-left">
<view class="task-icon">📝</view>
<text class="task-title">{{ task.zpmc || '作品任务' }}</text>
<view class="task-icon">{{ activeTab === 0 ? '📝' : '📚' }}</view>
<text class="task-title">{{ task.zpmc || (activeTab === 0 ? '作品任务' : '作品资料') }}</text>
</view>
<view class="status-badge" :class="getStatusClass(task.zpzt)">
<text class="status-text">{{ getStatusText(task.zpzt) }}</text>
@ -76,11 +81,15 @@
</view>
<view class="card-footer">
<button class="action-btn challenge-btn" @click.stop="startChallenge(task)">
<button
v-if="activeTab === 0"
class="action-btn challenge-btn"
@click.stop="startChallenge(task)"
>
开始挑战
</button>
<button class="action-btn detail-btn" @click.stop="viewTaskDetail(task)">
详情
<button class="action-btn detail-btn" @click.stop="activeTab === 0 ? viewTaskDetail(task) : viewResourceDetail(task)">
{{ activeTab === 0 ? '详情' : '查看资料' }}
</button>
</view>
</view>
@ -88,7 +97,7 @@
<view v-else class="empty-state">
<view class="empty-icon">📭</view>
<text class="empty-text">暂无作品任务</text>
<text class="empty-text">{{ activeTab === 0 ? '暂无作品任务' : '暂无作品资源' }}</text>
</view>
</scroll-view>
</view>
@ -96,7 +105,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
import { ref, computed, onUnmounted } from "vue";
import { onShow, onLoad } from "@dcloudio/uni-app";
import { useUserStore } from "@/store/modules/user";
import { zpzxFindByKcParamsApi } from "@/api/base/server";
@ -114,9 +123,21 @@ interface TaskItem {
kcId?: string;
xsId?: string;
ispj?: string; // A: B:
/** SUBMIT 任务提交VIEW 资料查看 */
rwms?: string;
}
const taskList = ref<TaskItem[]>([]);
const activeTab = ref(0);
const isResourceTask = (t: TaskItem) =>
t.rwms != null && String(t.rwms).trim().toUpperCase() === "VIEW";
const submitTasks = computed(() => taskList.value.filter((t) => !isResourceTask(t)));
const resourceTasks = computed(() => taskList.value.filter((t) => isResourceTask(t)));
const displayList = computed(() =>
activeTab.value === 0 ? submitTasks.value : resourceTasks.value
);
const isLoading = ref(false);
const kcId = ref<string>('');
const xsId = ref<string>('');
@ -252,7 +273,7 @@ const startChallenge = (task: TaskItem) => {
}
// ispj=B
const otherLocked = taskList.value.find(
const otherLocked = submitTasks.value.find(
(t) => t.id !== task.id && isTaskEvaluated(t)
);
if (otherLocked) {
@ -284,7 +305,7 @@ const startChallenge = (task: TaskItem) => {
});
};
// -
// -
const viewTaskDetail = (task: TaskItem) => {
const params = new URLSearchParams();
params.append('zpId', task.zpId);
@ -299,6 +320,27 @@ const viewTaskDetail = (task: TaskItem) => {
});
};
/** 作品资源(资料查看)详情 */
const viewResourceDetail = (task: TaskItem) => {
const params = new URLSearchParams();
params.append('zpId', task.zpId);
if (task.kcId || kcId.value) {
params.append('kcId', task.kcId || kcId.value);
}
if (task.xsId || xsId.value) {
params.append('xsId', task.xsId || xsId.value);
}
uni.navigateTo({
url: `/pages/base/xszp/resource-detail?${params.toString()}`
});
};
const onTaskCardClick = (task: TaskItem) => {
if (activeTab.value === 1) {
viewResourceDetail(task);
}
};
const getStatusClass = (status: string) => {
switch (status) {
case 'A':
@ -413,33 +455,37 @@ const formatTime = (timeStr?: string) => {
}
}
//
.selection-tip {
background: linear-gradient(135deg, #e6f4ff 0%, #f0f8ff 100%);
margin: 0;
padding: 16px;
box-shadow: 0 2px 8px rgba(78, 115, 223, 0.1);
.tip-content {
.tab-bar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
.tip-title {
font-size: 17px;
font-weight: 700;
color: #2c3e50;
line-height: 1.4;
background: #ffffff;
margin: 0 16px;
margin-top: -10px;
margin-bottom: 12px;
border-radius: 12px;
padding: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
z-index: 2;
}
.tip-desc {
font-size: 14px;
color: #4e73df;
line-height: 1.5;
font-weight: 500;
.tab-item {
flex: 1;
text-align: center;
padding: 10px 0;
border-radius: 10px;
transition: background 0.2s ease, color 0.2s ease;
.tab-text {
font-size: 15px;
color: #666;
}
&.active {
background: linear-gradient(135deg, #4e73df 0%, #2e59d9 100%);
.tab-text {
color: #ffffff;
font-weight: 600;
}
}
}
@ -489,6 +535,10 @@ const formatTime = (timeStr?: string) => {
position: relative;
overflow: visible;
&--resource {
cursor: pointer;
}
&:active {
transform: translateY(2px) scale(0.98);
box-shadow:

View File

@ -0,0 +1,445 @@
<!-- 作品资源详情资料查看 -->
<template>
<BasicLayout>
<view class="task-detail-page">
<view v-if="isLoading" class="loading-indicator">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else class="content-wrapper">
<view class="page-inner">
<view class="resource-meta-card">
<view class="meta-card-title">资源内容</view>
<view class="meta-block">
<text class="meta-label">资源名称</text>
<text class="meta-value meta-value--title">{{ taskInfo.zpmc || '作品资料' }}</text>
</view>
<view v-if="taskInfo.zpms" class="meta-block meta-block--desc">
<text class="meta-label">资源描述</text>
<text class="meta-value meta-value--body">{{ taskInfo.zpms }}</text>
</view>
</view>
<view v-if="taskInfo.fileUrl" class="attachment-card">
<view class="attachment-card-head">
<text class="attachment-icon">📎</text>
<view class="attachment-head-texts">
<text class="attachment-title">附件资料</text>
<text class="attachment-hint">点击下方文件名称即可预览或下载</text>
</view>
</view>
<view class="attachment-body">
<BasicFilePreview
:file-url="taskInfo.fileUrl"
:file-name="taskInfo.fileName"
:file-format="taskInfo.fileFormat"
/>
</view>
</view>
<view v-if="taskItemList.length > 0" class="task-items-section">
<view class="section-title">附加说明</view>
<view
v-for="(item, index) in taskItemList"
:key="item.id || index"
class="task-item-card"
>
<view class="item-header">
<text class="item-number">{{ index + 1 }}</text>
<text class="item-title">{{ item.zpbt || '说明' }}</text>
<view v-if="item.isbt === 1 || item.isbt === true" class="required-badge">
<text class="required-text">必填</text>
</view>
</view>
<view v-if="item.remark" class="item-remark">
<text class="remark-text">{{ item.remark }}</text>
</view>
<view class="item-type">
<text class="type-label">类型</text>
<text class="type-value">{{ getTaskTypeText(item.zpfl) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { zpFindDetailByIdApi } from "@/api/base/server";
import BasicFilePreview from "@/components/BasicFile/preview.vue";
interface TaskInfo {
id?: string;
zpmc?: string;
zpms?: string;
zpkstime?: string;
zpjstime?: string;
fileUrl?: string;
fileName?: string;
fileFormat?: string;
[key: string]: any;
}
interface TaskItem {
id: string;
zpbt?: string;
zpfl?: string;
isbt?: number | boolean;
remark?: string;
zpfldm?: string;
[key: string]: any;
}
const isLoading = ref(false);
const taskInfo = ref<TaskInfo>({});
const taskItemList = ref<TaskItem[]>([]);
onLoad(async (options) => {
const zpId = options?.zpId || '';
if (!zpId) {
uni.showToast({ title: '缺少资源编号', icon: 'error' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
await loadResourceDetail(zpId);
});
const loadResourceDetail = async (zpId: string) => {
try {
isLoading.value = true;
const response: any = await zpFindDetailByIdApi({ id: zpId });
const detailData = response?.result || response?.data || response;
if (!detailData) {
throw new Error('未找到资料');
}
const zpData = detailData.zp || {};
taskInfo.value = zpData;
taskItemList.value = detailData.zplxList || [];
} catch (error) {
console.error('加载资源详情失败:', error);
uni.showToast({
title: error instanceof Error ? error.message : '加载失败,请重试',
icon: 'error'
});
} finally {
isLoading.value = false;
}
};
const getTaskTypeText = (type?: string) => {
const typeMap: Record<string, string> = {
'sctp': '上传图片',
'scsp': '上传视频',
'scwd': '上传文档',
'fwb': '富文本',
'text': '文本',
'dxsx': '多项选择',
'dxxz': '单项选择'
};
return typeMap[type || ''] || '未知类型';
};
</script>
<style scoped lang="scss">
.task-detail-page {
background: linear-gradient(180deg, #eef2fb 0%, #f4f5f7 120px, #f4f5f7 100%);
min-height: 100vh;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4e73df;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: #999;
font-size: 14px;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.content-wrapper {
padding-bottom: 24px;
}
.page-inner {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.resource-meta-card {
padding: 18px 16px;
background: #fff;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(46, 89, 217, 0.08);
border: 1px solid rgba(78, 115, 223, 0.12);
}
.meta-card-title {
font-size: 17px;
font-weight: 700;
color: #1a1a2e;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 2px solid #4e73df;
}
.meta-block {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
&--desc {
padding-top: 4px;
border-top: 1px solid #f0f2f5;
}
}
.meta-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.meta-value {
color: #374151;
word-break: break-word;
&--title {
font-size: 18px;
font-weight: 700;
color: #111827;
line-height: 1.4;
}
&--body {
font-size: 15px;
line-height: 1.7;
color: #4b5563;
}
}
.attachment-card {
background: linear-gradient(145deg, #ffffff 0%, #f8faff 100%);
border-radius: 14px;
border: 1px solid rgba(78, 115, 223, 0.22);
box-shadow: 0 6px 24px rgba(46, 89, 217, 0.12);
overflow: hidden;
}
.attachment-card-head {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 16px 12px;
background: linear-gradient(135deg, rgba(78, 115, 223, 0.1) 0%, rgba(78, 115, 223, 0.02) 100%);
}
.attachment-icon {
font-size: 28px;
line-height: 1;
}
.attachment-head-texts {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.attachment-title {
font-size: 17px;
font-weight: 700;
color: #1e3a5f;
}
.attachment-hint {
font-size: 13px;
color: #5c6b8a;
line-height: 1.45;
}
.attachment-body {
padding: 8px 12px 16px;
}
.task-items-section {
padding: 18px 16px;
background: #fff;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(46, 89, 217, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #4e73df;
padding-bottom: 5px;
}
.task-item-card {
padding: 12px;
margin-bottom: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #4e73df;
&:last-child {
margin-bottom: 0;
}
.item-header {
display: flex;
align-items: center;
margin-bottom: 8px;
.item-number {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background: #4e73df;
color: #fff;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
margin-right: 8px;
flex-shrink: 0;
}
.item-title {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #333;
}
.required-badge {
padding: 2px 8px;
background: #fff3cd;
color: #856404;
border-radius: 4px;
font-size: 11px;
margin-left: 8px;
.required-text {
font-weight: 500;
}
}
}
.item-remark {
margin-bottom: 8px;
padding-left: 32px;
.remark-text {
font-size: 13px;
color: #666;
line-height: 1.5;
}
}
.item-type {
padding-left: 32px;
font-size: 13px;
.type-label {
color: #999;
}
.type-value {
color: #4e73df;
font-weight: 500;
}
}
}
}
/* 放大附件列表点击区域与字号,便于查阅 */
.attachment-body :deep(.basic-file-preview .file-list) {
display: flex;
flex-direction: column;
gap: 12px;
}
.attachment-body :deep(.basic-file-preview .file-item) {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 0;
padding: 14px 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
min-height: 52px;
}
.attachment-body :deep(.basic-file-preview .file-item:active) {
background: #f3f4f6;
}
.attachment-body :deep(.basic-file-preview .file-label) {
font-size: 14px;
color: #6b7280;
margin-right: 10px;
font-weight: 500;
}
.attachment-body :deep(.basic-file-preview .file-name) {
font-size: 16px;
font-weight: 600;
line-height: 1.5;
padding: 4px 0;
color: #2563eb;
text-decoration: none;
border-bottom: 1px solid rgba(37, 99, 235, 0.35);
}
.attachment-body :deep(.basic-file-preview .file-name:active) {
color: #1d4ed8;
}
</style>

View File

@ -5,6 +5,16 @@
import { KK_FILE_VIEW_URL } from '@/config';
export const isVideo = (fileType: string): boolean => {
const videoTypes = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', '3gp', 'm4v'];
return videoTypes.includes(fileType.toLowerCase());
};
export const isImage = (fileType: string): boolean => {
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
return imageTypes.includes(fileType.toLowerCase());
};
// 检查是否是压缩包文件
const isCompressedFile = (fileName: string): boolean => {
const compressedExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'];
@ -13,7 +23,7 @@ const isCompressedFile = (fileName: string): boolean => {
};
// 检查文件是否可以预览
const canPreview = (fileName: string): boolean => {
export const canPreview = (fileName: string): boolean => {
const previewableExtensions = [
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'txt', 'rtf', 'odt', 'ods', 'odp',
@ -269,3 +279,15 @@ export const previewVideo = (videoUrl: string, videoTitle: string) => {
});
});
};
// 图片预览
export const previewImage = (imageUrl: string) => {
return new Promise((resolve) => {
uni.previewImage({
urls: [imageUrl],
current: imageUrl,
success: resolve,
fail: resolve,
});
});
};