功能调整
This commit is contained in:
parent
65a5992e57
commit
eaf54b9048
@ -261,6 +261,76 @@ export const jsFindPageJfApi = async (params: any) => {
|
||||
return await get("/api/js/findPageJf", params);
|
||||
};
|
||||
|
||||
// 教师积分类型汇总
|
||||
export const jfSummaryByJfTypeApi = async (params: any) => {
|
||||
return await get("/api/jf/summaryByJfType", params);
|
||||
};
|
||||
|
||||
// 顶级类型及下级可加分结构
|
||||
export const jfTypeStructureApi = async (params: any) => {
|
||||
return await get("/api/jf/typeStructure", params);
|
||||
};
|
||||
|
||||
// 顶级类型下的积分结构(按类型节点汇总)
|
||||
export const jfScoreStructureApi = async (params: any) => {
|
||||
return await get("/api/jf/scoreStructure", params);
|
||||
};
|
||||
|
||||
// 查询积分记录列表
|
||||
export const jfFindPageApi = async (params: any) => {
|
||||
return await get("/api/jf/findPage", params);
|
||||
};
|
||||
|
||||
// 积分申请(走审批流程)
|
||||
export const jfSqApi = async (params: any) => {
|
||||
return await post("/api/jf/sq", params);
|
||||
};
|
||||
|
||||
// 积分待办/已办列表
|
||||
export const jfFindUserTodosPageApi = async (params: any) => {
|
||||
return await get("/api/jf/findUserTodosPage", params);
|
||||
};
|
||||
|
||||
// 保存积分记录(新增/修改)
|
||||
export const jfSaveApi = async (params: any) => {
|
||||
return await post("/api/jf/save", params);
|
||||
};
|
||||
|
||||
// 积分审批-同意/驳回
|
||||
export const jfSpApi = async (params: any) => {
|
||||
return await post("/api/jf/sp", params);
|
||||
};
|
||||
|
||||
// 积分审批-转办
|
||||
export const jfTransferApi = async (params: any) => {
|
||||
return await post("/api/jf/transfer", params);
|
||||
};
|
||||
|
||||
// 积分审批-终止
|
||||
export const jfStopApi = async (params: any) => {
|
||||
return await post("/api/jf/stop", params);
|
||||
};
|
||||
|
||||
// 积分审批-重新提交
|
||||
export const jfReSubmitApi = async (params: any) => {
|
||||
return await post("/api/jf/reSubmit", params);
|
||||
};
|
||||
|
||||
// 积分审批-流程详情
|
||||
export const jfFlowByIdApi = async (params: any) => {
|
||||
return await get("/api/jf/flowById", params);
|
||||
};
|
||||
|
||||
// 查询积分项目列表(树形结构)
|
||||
export const jfTypeFindPageApi = async (params: any) => {
|
||||
return await get("/api/jfType/findPage", params);
|
||||
};
|
||||
|
||||
// 查询积分规则列表
|
||||
export const jfRuleFindPageApi = async (params: any) => {
|
||||
return await get("/api/jfRule/findPage", params);
|
||||
};
|
||||
|
||||
// 检查项查询
|
||||
export const inspectItemFindAllApi = async (params: any) => {
|
||||
return await get("/api/inspectItem/findAlls", params);
|
||||
|
||||
@ -24,3 +24,11 @@ export const srFindPageApi = async (params: any) => {
|
||||
return await get("/api/srTsRecord/findPage", params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过记录ID获取贺卡详情
|
||||
* @param params { recordId: 记录ID, receiverId: 接收人ID }
|
||||
*/
|
||||
export const srGetCardDetailByRecordApi = async (params: { recordId: string; receiverId?: string }) => {
|
||||
return await get("/api/srTsRecord/getCardDetailByRecord", params);
|
||||
};
|
||||
|
||||
|
||||
@ -47,6 +47,14 @@ export const findAllZw = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -711,7 +711,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/JiFenPingJia",
|
||||
"path": "pages/view/routine/JiFenPingJia/jfself/JiFenPingJia",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分评价",
|
||||
"enablePullDownRefresh": false
|
||||
@ -791,12 +791,40 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/detail",
|
||||
"path": "pages/view/routine/JiFenPingJia/jfself/apply",
|
||||
"style": {
|
||||
"navigationBarTitleText": "评价详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfself/MyScoreDetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfself/IntegralApply",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分申请",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfsp/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分审批",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfsp/JfFlow",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分审批",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/kefuxuncha/xcXkList",
|
||||
"style": {
|
||||
@ -958,20 +986,7 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/PersonalHonor",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人荣誉",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/PublicClassAwards",
|
||||
"style": {
|
||||
"navigationBarTitleText": "公开课获奖",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/view/hr/teacherProfile/RecordMaterials",
|
||||
"style": {
|
||||
@ -1081,19 +1096,29 @@
|
||||
{
|
||||
"path": "pages/view/routine/sr/envelope",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "生日祝福",
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundColor": "#ff9a9e"
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/sr/card",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "生日祝福",
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundColor": "#ff9a9e"
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/sr/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "生日清单",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/sr/viewCard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "生日祝福",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -385,6 +385,7 @@ const sections = reactive<Section[]>([
|
||||
title: "功能应用",
|
||||
permissionKey: "gnyycommon", // 整个功能区域的权限编码
|
||||
items: [
|
||||
|
||||
{
|
||||
id: "gnyy1",
|
||||
icon: "kctb",
|
||||
@ -641,6 +642,40 @@ const sections = reactive<Section[]>([
|
||||
permissionKey: "routine-tzfb",
|
||||
path: "/pages/view/routine/tz/index",
|
||||
},
|
||||
{
|
||||
id: "xxfb-srqd",
|
||||
icon: "srqd",
|
||||
text: "生日清单",
|
||||
show: true,
|
||||
permissionKey: "routine-srqd",
|
||||
path: "/pages/view/routine/sr/list",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "routine-jf",
|
||||
icon: "jfpj",
|
||||
text: "业绩积分",
|
||||
show: true,
|
||||
permissionKey: "routine-jf",
|
||||
isFolder: true,
|
||||
folderItems: [
|
||||
{
|
||||
id: "routine-jf-pj",
|
||||
icon: "jfpj",
|
||||
text: "积分评价",
|
||||
show: true,
|
||||
permissionKey: "routine-jfpj",
|
||||
path: "/pages/view/routine/JiFenPingJia/jfself/JiFenPingJia",
|
||||
},
|
||||
{
|
||||
id: "routine-jf-sp",
|
||||
icon: "gw",
|
||||
text: "积分审批",
|
||||
show: true,
|
||||
permissionKey: "routine-jfsp",
|
||||
path: "/pages/view/routine/JiFenPingJia/jfsp/index",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -650,14 +685,6 @@ const sections = reactive<Section[]>([
|
||||
title: "教学常规",
|
||||
permissionKey: "routine", // 整个常规区域的权限编码
|
||||
items: [
|
||||
{
|
||||
id: "r2",
|
||||
icon: "jfpj",
|
||||
text: "积分评价",
|
||||
show: true,
|
||||
permissionKey: "routine-jfpj", // 积分评价权限编码
|
||||
path: "/pages/view/routine/JiFenPingJia/JiFenPingJia",
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
icon: "gzltj",
|
||||
|
||||
@ -1,357 +0,0 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="p-15">
|
||||
<view v-if="education.xl.length > 0">
|
||||
<template v-for="(item, index) in education.xl" :key="index">
|
||||
<view class="po-re mb-15">
|
||||
<BasicForm
|
||||
v-model="item.value"
|
||||
:schema="getSchemaForIndex(index)"
|
||||
:index="index"
|
||||
:key="`form-${index}-${forceUpdateKey}`"
|
||||
:formsProps="{ labelWidth: 100 }"
|
||||
/>
|
||||
<view
|
||||
@click="deleteMemberFamily(index, item.value)"
|
||||
class="delete-icon"
|
||||
>
|
||||
<BasicIcon type="clear" size="30" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<view
|
||||
class="flex-row items-center justify-center pb-10 pt-5"
|
||||
style="border: 1px solid #e8e8e8"
|
||||
@click="addEducation"
|
||||
>
|
||||
<uni-icons type="plus" size="16" color="#447ADE"></uni-icons>
|
||||
<view class="ml-5 cor-447ADE">新增</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="flex-row items-center pb-10 pt-5">
|
||||
<u-button
|
||||
text="提交"
|
||||
class="mx-15"
|
||||
type="primary"
|
||||
@click="submit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from "@/utils/uniapp";
|
||||
import { cloneDeep, map } from "lodash";
|
||||
import { fractionRuleApi } from "@/api/base/server";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const { getJs, getUser } = useUserStore();
|
||||
|
||||
// 主数据
|
||||
const education = reactive<any>({
|
||||
xl: [{ value: {} }],
|
||||
});
|
||||
|
||||
// 荣誉类别数据缓存
|
||||
const honorCategories = ref<any[]>([]);
|
||||
|
||||
// 每个表单项对应的获奖级别数据
|
||||
const awardLevelsMap = reactive<Record<number, any>>({});
|
||||
|
||||
// 强制重新渲染的键
|
||||
const forceUpdateKey = ref(0);
|
||||
|
||||
// 基础表单配置
|
||||
const baseSchema = [
|
||||
{
|
||||
field: "rymc",
|
||||
label: "荣誉名称",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hilb_id",
|
||||
label: "荣誉类别",
|
||||
component: "BasicPicker",
|
||||
componentProps: {
|
||||
api: fractionRuleApi,
|
||||
rangeKey: "inspectStandard",
|
||||
savaKey: "id",
|
||||
ok: (selectedIndex: number, form: any, list: any, attrs: any) => {
|
||||
const selectedCategory = list[selectedIndex];
|
||||
const formIndex = attrs.index;
|
||||
|
||||
// 更新当前表单项的获奖级别选项
|
||||
updateAwardLevels(formIndex, selectedCategory);
|
||||
|
||||
// 清空已选择的获奖级别(如果之前有选择的话)
|
||||
if (education.xl[formIndex].value.hjjbId) {
|
||||
education.xl[formIndex].value.hjjbId = "";
|
||||
}
|
||||
// 强制重新渲染
|
||||
forceUpdateKey.value++;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "hjjbId",
|
||||
label: "获奖级别",
|
||||
component: "BasicPicker",
|
||||
componentProps: {
|
||||
range: [],
|
||||
rangeKey: "name",
|
||||
savaKey: "id",
|
||||
open: (value: any, attrs: any, model: any) => {
|
||||
const formIndex = attrs.index;
|
||||
// 检查是否已选择荣誉类别
|
||||
if (!model?.hilb_id) {
|
||||
showToast({
|
||||
title: "请先选择荣誉类别",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有对应的获奖级别数据
|
||||
const awardLevels = awardLevelsMap[formIndex] || [];
|
||||
|
||||
if (awardLevels.length === 0) {
|
||||
showToast({
|
||||
title: "该荣誉类别暂无获奖级别数据",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "bjdw",
|
||||
label: "颁奖单位",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hjtime",
|
||||
label: "获奖时间",
|
||||
component: "BasicDateTimes",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "jf",
|
||||
label: "积分",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hjfjId",
|
||||
label: "上传证书",
|
||||
component: "BasicUpload",
|
||||
itemProps: {
|
||||
labelPosition: "top",
|
||||
},
|
||||
componentProps: {},
|
||||
},
|
||||
];
|
||||
|
||||
// 为每个表单项生成动态的schema
|
||||
const getSchemaForIndex = (index: number) => {
|
||||
const schema = cloneDeep(baseSchema);
|
||||
|
||||
// 更新获奖级别的选项数据
|
||||
const hjjbField = schema.find((item) => item.field === "hjjbId");
|
||||
if (hjjbField) {
|
||||
hjjbField.componentProps.range = awardLevelsMap[index] || [];
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
// 更新指定表单项的获奖级别选项
|
||||
const updateAwardLevels = (formIndex: number, category: any) => {
|
||||
if (category?.ruleItemList && category.ruleItemList.length > 0) {
|
||||
awardLevelsMap[formIndex] = category.ruleItemList;
|
||||
} else {
|
||||
awardLevelsMap[formIndex] = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化荣誉类别数据
|
||||
const initHonorCategories = async () => {
|
||||
try {
|
||||
const result = await fractionRuleApi();
|
||||
honorCategories.value = result.result || result || [];
|
||||
} catch (error) {
|
||||
console.error("获取荣誉类别数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化回显数据
|
||||
const initEchoData = async () => {
|
||||
await initHonorCategories();
|
||||
|
||||
// 为每个已有数据的表单项初始化获奖级别选项
|
||||
education.xl.forEach((formItem: any, index: number) => {
|
||||
if (formItem.value?.hilb_id) {
|
||||
const category = honorCategories.value.find(
|
||||
(cat: any) => cat.id === formItem.value.hilb_id
|
||||
);
|
||||
|
||||
if (category) {
|
||||
// 更新获奖级别选项
|
||||
updateAwardLevels(index, category);
|
||||
} else {
|
||||
console.log(`未找到荣誉类别 ID: ${formItem.value.hilb_id}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`表单项 ${index} 没有荣誉类别 ID`);
|
||||
}
|
||||
});
|
||||
|
||||
// 强制重新渲染以确保获奖级别能正确显示
|
||||
forceUpdateKey.value++;
|
||||
};
|
||||
|
||||
// 添加新的教育项
|
||||
function addEducation() {
|
||||
const newIndex = education.xl.length;
|
||||
education.xl.push({ value: {} });
|
||||
|
||||
// 为新项初始化空的获奖级别选项
|
||||
awardLevelsMap[newIndex] = [];
|
||||
}
|
||||
|
||||
// 删除教育项
|
||||
function deleteMemberFamily(index: number, item: any) {
|
||||
// 删除对应的获奖级别数据
|
||||
delete awardLevelsMap[index];
|
||||
|
||||
// 重新整理awardLevelsMap的键值
|
||||
const newAwardLevelsMap: Record<number, any[]> = {};
|
||||
education.xl.forEach((_: any, i: number) => {
|
||||
if (i < index) {
|
||||
newAwardLevelsMap[i] = awardLevelsMap[i] || [];
|
||||
} else if (i > index) {
|
||||
newAwardLevelsMap[i - 1] = awardLevelsMap[i] || [];
|
||||
}
|
||||
});
|
||||
|
||||
// 删除表单项
|
||||
education.xl.splice(index, 1);
|
||||
|
||||
// 更新awardLevelsMap
|
||||
Object.keys(awardLevelsMap).forEach(
|
||||
(key: string) => delete awardLevelsMap[Number(key)]
|
||||
);
|
||||
Object.assign(awardLevelsMap, newAwardLevelsMap);
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
async function submit() {
|
||||
try {
|
||||
// 验证表单数据
|
||||
const grRyList = map(education.xl, (item) => {
|
||||
return { ...item.value, hjlxId: "GRRY" };
|
||||
});
|
||||
|
||||
if (grRyList.length === 0) {
|
||||
showToast({
|
||||
title: "请至少添加一条荣誉记录",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
for (let i = 0; i < grRyList.length; i++) {
|
||||
const item = grRyList[i];
|
||||
if (!item.rymc) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请填写荣誉名称`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item.hilb_id) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请选择荣誉类别`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item.hjjbId) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请选择获奖级别`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item.bjdw) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请填写颁奖单位`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item.hjtime) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请选择获奖时间`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!item.jf) {
|
||||
showToast({
|
||||
title: `第${i + 1}条记录:请填写积分`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建提交参数
|
||||
const params = {
|
||||
id: null, // 新增申请
|
||||
jsId: getJs.id,
|
||||
jsName: getJs.jsxm,
|
||||
grRyList: grRyList,
|
||||
// 积分申请相关字段
|
||||
inspectStandard: "个人荣誉申请",
|
||||
scoreType: "1", // 加分
|
||||
examineTime: new Date(),
|
||||
remark: "个人荣誉积分申请",
|
||||
};
|
||||
|
||||
uni.showLoading({ title: "提交中..." });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("提交积分申请失败:", error);
|
||||
showToast({
|
||||
title: "提交失败,请稍后重试",
|
||||
icon: "none"
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
initHonorCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
right: -13px;
|
||||
top: -14px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="p-15">
|
||||
<view v-if="education.xl.length > 0">
|
||||
<template v-for="(item, index) in education.xl" :key="index">
|
||||
<view class="po-re mb-15">
|
||||
<BasicForm
|
||||
v-model="item.value"
|
||||
:schema="getSchemaForIndex(index)"
|
||||
:index="index"
|
||||
:key="`form-${index}-${forceUpdateKey}`"
|
||||
:formsProps="{ labelWidth: 100 }"
|
||||
/>
|
||||
<view
|
||||
@click="deleteMemberFamily(index, item.value)"
|
||||
class="delete-icon"
|
||||
>
|
||||
<BasicIcon type="clear" size="30" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<view
|
||||
class="flex-row items-center justify-center pb-10 pt-5"
|
||||
style="border: 1px solid #e8e8e8"
|
||||
@click="addEducation"
|
||||
>
|
||||
<uni-icons type="plus" size="16" color="#447ADE"></uni-icons>
|
||||
<view class="ml-5 cor-447ADE">新增</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="flex-row items-center pb-10 pt-5">
|
||||
<u-button text="提交" class="mx-15" type="primary" @click="submit" />
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { fractionRuleApi1 } from "@/api/base/server";
|
||||
import { showToast } from "@/utils/uniapp";
|
||||
import { cloneDeep, map } from "lodash";
|
||||
|
||||
// 主数据
|
||||
const education = reactive<any>({
|
||||
xl: [{ value: {} }],
|
||||
});
|
||||
|
||||
// 荣誉类别数据缓存
|
||||
const honorCategories = ref<any[]>([]);
|
||||
|
||||
// 每个表单项对应的获奖级别数据
|
||||
const awardLevelsMap = reactive<Record<number, any>>({});
|
||||
|
||||
// 强制重新渲染的键
|
||||
const forceUpdateKey = ref(0);
|
||||
|
||||
// 基础表单配置
|
||||
const baseSchema = [
|
||||
{
|
||||
field: "rymc",
|
||||
label: "荣誉名称",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hilb_id",
|
||||
label: "荣誉类别",
|
||||
component: "BasicPicker",
|
||||
componentProps: {
|
||||
api: fractionRuleApi1,
|
||||
rangeKey: "inspectStandard",
|
||||
savaKey: "id",
|
||||
ok: (selectedIndex: number, form: any, list: any, attrs: any) => {
|
||||
const selectedCategory = list[selectedIndex];
|
||||
const formIndex = attrs.index;
|
||||
|
||||
// 更新当前表单项的获奖级别选项
|
||||
updateAwardLevels(formIndex, selectedCategory);
|
||||
|
||||
// 清空已选择的获奖级别(如果之前有选择的话)
|
||||
if (education.xl[formIndex].value.xm) {
|
||||
education.xl[formIndex].value.xm = "";
|
||||
}
|
||||
|
||||
// 强制重新渲染
|
||||
forceUpdateKey.value++;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "xm",
|
||||
label: "获奖级别",
|
||||
component: "BasicPicker",
|
||||
componentProps: {
|
||||
range: [],
|
||||
rangeKey: "name",
|
||||
savaKey: "id",
|
||||
open: (value: any, attrs: any, model: any) => {
|
||||
const formIndex = attrs.index;
|
||||
|
||||
// 检查是否已选择荣誉类别
|
||||
if (!model?.hilb_id) {
|
||||
showToast({
|
||||
title: "请先选择荣誉类别",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有对应的获奖级别数据
|
||||
const awardLevels = awardLevelsMap[formIndex] || [];
|
||||
|
||||
if (awardLevels.length === 0) {
|
||||
showToast({
|
||||
title: "该荣誉类别暂无获奖级别数据",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "bjdw",
|
||||
label: "颁奖单位",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hjtime",
|
||||
label: "获奖时间",
|
||||
component: "BasicDateTimes",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "jf",
|
||||
label: "积分",
|
||||
component: "BasicInput",
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: "hjfjId",
|
||||
label: "上传证书",
|
||||
component: "BasicUpload",
|
||||
required: true,
|
||||
itemProps: {
|
||||
labelPosition: "top",
|
||||
},
|
||||
componentProps: {},
|
||||
},
|
||||
];
|
||||
|
||||
// 为每个表单项生成动态的schema
|
||||
const getSchemaForIndex = (index: number) => {
|
||||
const schema = cloneDeep(baseSchema);
|
||||
|
||||
// 更新获奖级别的选项数据
|
||||
const hjjbField = schema.find((item) => item.field === "xm");
|
||||
if (hjjbField) {
|
||||
hjjbField.componentProps.range = awardLevelsMap[index] || [];
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
// 更新指定表单项的获奖级别选项
|
||||
const updateAwardLevels = (formIndex: number, category: any) => {
|
||||
if (category?.ruleItemList && category.ruleItemList.length > 0) {
|
||||
awardLevelsMap[formIndex] = category.ruleItemList;
|
||||
} else {
|
||||
awardLevelsMap[formIndex] = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化荣誉类别数据
|
||||
const initHonorCategories = async () => {
|
||||
try {
|
||||
const result = await fractionRuleApi1();
|
||||
honorCategories.value = result.result || result || [];
|
||||
} catch (error) {
|
||||
console.error("获取荣誉类别数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化回显数据
|
||||
const initEchoData = async () => {
|
||||
await initHonorCategories();
|
||||
|
||||
// 为每个已有数据的表单项初始化获奖级别选项
|
||||
education.xl.forEach((formItem: any, index: number) => {
|
||||
if (formItem.value?.hilb_id) {
|
||||
const category = honorCategories.value.find(
|
||||
(cat: any) => cat.id === formItem.value.hilb_id
|
||||
);
|
||||
|
||||
if (category) {
|
||||
// 更新获奖级别选项
|
||||
updateAwardLevels(index, category);
|
||||
} else {
|
||||
console.log(`未找到荣誉类别 ID: ${formItem.value.hilb_id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 强制重新渲染以确保获奖级别能正确显示
|
||||
forceUpdateKey.value++;
|
||||
};
|
||||
|
||||
// 添加新的教育项
|
||||
function addEducation() {
|
||||
const newIndex = education.xl.length;
|
||||
education.xl.push({ value: {} });
|
||||
|
||||
// 为新项初始化空的获奖级别选项
|
||||
awardLevelsMap[newIndex] = [];
|
||||
}
|
||||
|
||||
// 删除教育项
|
||||
function deleteMemberFamily(index: number, item: any) {
|
||||
// 删除对应的获奖级别数据
|
||||
delete awardLevelsMap[index];
|
||||
|
||||
// 重新整理awardLevelsMap的键值
|
||||
const newAwardLevelsMap: Record<number, any[]> = {};
|
||||
education.xl.forEach((_: any, i: number) => {
|
||||
if (i < index) {
|
||||
newAwardLevelsMap[i] = awardLevelsMap[i] || [];
|
||||
} else if (i > index) {
|
||||
newAwardLevelsMap[i - 1] = awardLevelsMap[i] || [];
|
||||
}
|
||||
});
|
||||
|
||||
// 删除表单项
|
||||
education.xl.splice(index, 1);
|
||||
|
||||
// 更新awardLevelsMap
|
||||
Object.keys(awardLevelsMap).forEach(
|
||||
(key: string) => delete awardLevelsMap[Number(key)]
|
||||
);
|
||||
Object.assign(awardLevelsMap, newAwardLevelsMap);
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
function submit() {
|
||||
const gkkRyList = map(education.xl, (item) => {
|
||||
return { ...item.value, hjlxId: "GKKHJQK" };
|
||||
});
|
||||
}
|
||||
|
||||
// // 初始化数据
|
||||
// const { getFile, setFile } = useDataStore();
|
||||
|
||||
// // 处理回显数据
|
||||
// if (getFile.gkkRyList && getFile.gkkRyList.length > 0) {
|
||||
// education.xl = map(getFile.gkkRyList, (item) => {
|
||||
// return { value: item };
|
||||
// });
|
||||
// }
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
// if (getFile.gkkRyList && getFile.gkkRyList.length > 0) {
|
||||
// // 有回显数据时,延迟初始化确保数据正确加载
|
||||
// nextTick(() => {
|
||||
// initEchoData();
|
||||
// });
|
||||
// } else {
|
||||
// // 无回显数据时,只需要初始化荣誉类别
|
||||
// initHonorCategories();
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
right: -13px;
|
||||
top: -14px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -1,426 +0,0 @@
|
||||
<template>
|
||||
<view class="detail-container">
|
||||
<view class="content-wrapper">
|
||||
<!-- 评分标准 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">评分标准</view>
|
||||
<view class="standard-text">
|
||||
1.一二年级考核语文、数学两个学科。平行班教学质量评价差距均在2分内的,按一...
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 考核评价 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">考核评价</text>
|
||||
<text class="detail-value">{{ detailData.evaluationType }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 积分类型 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">积分类型</text>
|
||||
<text class="detail-value">{{ detailData.scoreType }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 积分分值 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">积分分值</text>
|
||||
<text class="detail-value">{{ detailData.scoreValue }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 考核处室 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">考核处室</text>
|
||||
<text class="detail-value">{{ detailData.department }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 考核时间 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">考核时间</text>
|
||||
<text class="detail-value">{{ detailData.evaluationDate }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 考核打分 -->
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">考核打分</text>
|
||||
<text class="detail-value">{{ detailData.score }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 证明材料 -->
|
||||
<view class="detail-item file-section" v-if="detailData.files && detailData.files.length > 0">
|
||||
<text class="detail-label">证明材料</text>
|
||||
<view class="file-list">
|
||||
<view
|
||||
class="file-item"
|
||||
v-for="(file, index) in detailData.files"
|
||||
:key="index"
|
||||
@click="handlePreviewFile(file)"
|
||||
>
|
||||
<uni-icons type="paperplane" size="16" color="#409eff" />
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="mobile-btn" @click="goBack">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { imagUrl } from "@/utils";
|
||||
import { getByUserIdAndInspectItemIdApi } from "@/api/base/server";
|
||||
import {
|
||||
isVideo,
|
||||
isImage,
|
||||
canPreview,
|
||||
previewFile,
|
||||
previewVideo,
|
||||
previewImage,
|
||||
downloadFile
|
||||
} from "@/utils/filePreview";
|
||||
|
||||
interface DetailData {
|
||||
evaluationType: string;
|
||||
scoreType: string;
|
||||
scoreValue: number | string;
|
||||
department: string;
|
||||
evaluationDate: string;
|
||||
score: number | string;
|
||||
files: Array<{ name: string; url: string; resSuf?: string }>;
|
||||
}
|
||||
|
||||
// 详情数据
|
||||
const detailData = ref<DetailData>({
|
||||
evaluationType: "",
|
||||
scoreType: "",
|
||||
scoreValue: 0,
|
||||
department: "",
|
||||
evaluationDate: "",
|
||||
score: 0,
|
||||
files: [],
|
||||
});
|
||||
|
||||
// 页面参数
|
||||
const pageParams = ref({
|
||||
inspectItemId: '',
|
||||
userId: ''
|
||||
});
|
||||
|
||||
// 获取详情数据
|
||||
const loadDetailData = async () => {
|
||||
try {
|
||||
const { inspectItemId, userId } = pageParams.value;
|
||||
|
||||
console.log('页面参数:', pageParams.value);
|
||||
|
||||
if (!inspectItemId || !userId) {
|
||||
console.error('缺少必要参数');
|
||||
uni.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用API获取详情数据
|
||||
const res = await getByUserIdAndInspectItemIdApi({
|
||||
userId: userId,
|
||||
inspectItemId: inspectItemId
|
||||
});
|
||||
|
||||
console.log('API返回数据:', res);
|
||||
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
const data = res.result;
|
||||
|
||||
// 处理返回的数据结构
|
||||
detailData.value = {
|
||||
evaluationType: data.evaluations?.[0]?.itemName || "考核评价",
|
||||
scoreType: data.evaluations?.[0]?.scoreType === "1" ? "加分" : "扣分",
|
||||
scoreValue: data.evaluations?.[0]?.score || 0,
|
||||
department: data.evaluations?.[0]?.departmentName || "教科处",
|
||||
evaluationDate: data.evaluations?.[0]?.examineTime || "",
|
||||
score: data.score || 0,
|
||||
files: data.evaluations?.[0]?.pic ? [
|
||||
{
|
||||
name: "证明材料",
|
||||
url: data.evaluations[0].pic,
|
||||
resSuf: data.evaluations[0].pic.split(".").pop()?.toLowerCase() || ""
|
||||
}
|
||||
] : []
|
||||
};
|
||||
|
||||
console.log('处理后的详情数据:', detailData.value);
|
||||
} else {
|
||||
console.warn('API返回数据为空或格式不正确');
|
||||
uni.showToast({
|
||||
title: '未获取到详情数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取详情数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 文件预览处理(参考教学资源的预览方式)
|
||||
const handlePreviewFile = (file: { name: string; url: string; resSuf?: string }) => {
|
||||
const fileUrl = imagUrl(file.url);
|
||||
const fileName = file.name;
|
||||
const fileExt = file.resSuf || file.name.split(".").pop()?.toLowerCase() || "";
|
||||
|
||||
console.log('预览文件:', {
|
||||
name: fileName,
|
||||
url: fileUrl,
|
||||
ext: fileExt
|
||||
});
|
||||
|
||||
// 根据文件类型进行预览
|
||||
if (isVideo(fileExt)) {
|
||||
// 视频预览
|
||||
previewVideo(fileUrl, fileName)
|
||||
.then(() => {
|
||||
console.log('视频预览成功');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('视频预览失败:', error);
|
||||
// 预览失败时尝试下载
|
||||
handleDownloadFile(file);
|
||||
});
|
||||
} else if (isImage(fileExt)) {
|
||||
// 图片预览
|
||||
previewImage(fileUrl)
|
||||
.then(() => {
|
||||
console.log('图片预览成功');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('图片预览失败:', error);
|
||||
handleDownloadFile(file);
|
||||
});
|
||||
} else if (canPreview(fileExt)) {
|
||||
// 可预览文件
|
||||
previewFile(fileUrl, fileName, fileExt)
|
||||
.then(() => {
|
||||
console.log('文件预览成功');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('文件预览失败:', error);
|
||||
handleDownloadFile(file);
|
||||
});
|
||||
} else {
|
||||
// 不可预览文件,直接下载
|
||||
handleDownloadFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 文件下载处理
|
||||
const handleDownloadFile = (file: { name: string; url: string }) => {
|
||||
const fileUrl = imagUrl(file.url);
|
||||
const fileName = file.name;
|
||||
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: `是否下载文件: ${fileName}?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
downloadFile(fileUrl, fileName)
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: "下载成功",
|
||||
icon: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('下载失败:', error);
|
||||
uni.showToast({
|
||||
title: "下载失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// onLoad会自动执行,不需要在这里调用
|
||||
});
|
||||
|
||||
// 调用onLoad生命周期
|
||||
onLoad((options) => {
|
||||
console.log('onLoad被调用,参数:', options);
|
||||
|
||||
// 从URL获取inspectItemId
|
||||
const inspectItemId = options.inspectItemId || '';
|
||||
|
||||
// 从缓存获取教师ID
|
||||
let userId = '';
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
userId = userData.jsData.id;
|
||||
console.log('从缓存获取到教师ID:', userId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置页面参数
|
||||
pageParams.value = {
|
||||
inspectItemId: inspectItemId,
|
||||
userId: userId
|
||||
};
|
||||
|
||||
console.log('最终页面参数:', pageParams.value);
|
||||
|
||||
// 加载详情数据
|
||||
loadDetailData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 30rpx 30rpx 40rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.standard-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.detail-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.file-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.detail-label {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
width: 100%;
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 10rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
background-color: #d4f1ff;
|
||||
}
|
||||
|
||||
uni-icons {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background-color: #ffffff;
|
||||
border-top: 1rpx solid #eee;
|
||||
|
||||
.mobile-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #409eff;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background-color: #337ecc;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1491
src/pages/view/routine/JiFenPingJia/jfself/IntegralApply.vue
Normal file
1491
src/pages/view/routine/JiFenPingJia/jfself/IntegralApply.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,14 +3,7 @@
|
||||
<view class="container">
|
||||
<uni-card :is-shadow="false" is-full>
|
||||
<view class="header">
|
||||
<text class="score">我的得分: {{ totalScore }}</text>
|
||||
<view class="status">
|
||||
<text>我的状态: </text>
|
||||
<view class="status-indicator"></view>
|
||||
<view class="review-count" v-if="reviewingCountComputed > 0">
|
||||
{{ reviewingCountComputed }}项在审核
|
||||
</view>
|
||||
</view>
|
||||
<text class="score">业绩积分: {{ totalScore }}</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
@ -46,16 +39,10 @@
|
||||
<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"
|
||||
text="业绩填报"
|
||||
class="ml-15 mr-15"
|
||||
type="primary"
|
||||
@click="scgrry"
|
||||
/>
|
||||
<u-button
|
||||
text="公开课获奖申请"
|
||||
class="mr-15 mr-7"
|
||||
type="primary"
|
||||
@click="scgkkhj"
|
||||
@click="handleIntegralApply"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@ -65,47 +52,35 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { jsFindPageJfApi, inspectItemFindAllApi } from "@/api/base/server";
|
||||
import { jfSummaryByJfTypeApi } from "@/api/base/server";
|
||||
|
||||
interface EvaluationItem {
|
||||
id: string;
|
||||
category: string;
|
||||
value: number;
|
||||
isUnderReview: boolean;
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const evaluationItems = ref([]);
|
||||
const evaluationItems = ref<EvaluationItem[]>([]);
|
||||
const totalScore = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 计算正在审核的项目数量
|
||||
const reviewingCountComputed = computed(() => {
|
||||
return evaluationItems.value.filter(item => item.isUnderReview).length;
|
||||
});
|
||||
|
||||
// 获取检查项数据
|
||||
const loadInspectItems = async () => {
|
||||
try {
|
||||
const res = await inspectItemFindAllApi({ type: 2 });
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
return res.result;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取检查项失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取教师积分数据
|
||||
const loadTeacherScore = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 从本地缓存获取当前教师ID
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
let teacherId = '';
|
||||
let jsId = '';
|
||||
let userData = null;
|
||||
|
||||
if (userDataStr) {
|
||||
try {
|
||||
userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
teacherId = userData.jsData.id;
|
||||
console.log('当前教师ID:', teacherId);
|
||||
jsId = userData.jsData.id;
|
||||
console.log('当前教师ID:', jsId);
|
||||
console.log('教师信息:', userData.jsData);
|
||||
} else {
|
||||
console.error('未找到教师ID信息');
|
||||
@ -136,89 +111,29 @@ const loadTeacherScore = async () => {
|
||||
const params = {
|
||||
startTime: new Date().getFullYear() + '-01-01', // 当年开始时间
|
||||
endTime: new Date().getFullYear() + '-12-31', // 当年结束时间
|
||||
pageSize: 100, // 增大页面大小,确保能获取到当前教师数据
|
||||
pageNum: 1,
|
||||
id: teacherId // 使用教师ID过滤
|
||||
jsId: jsId // 使用教师ID过滤
|
||||
};
|
||||
|
||||
const res = await jsFindPageJfApi(params);
|
||||
const res: any = await jfSummaryByJfTypeApi(params);
|
||||
console.log('查询参数:', params);
|
||||
console.log('当前教师ID:', teacherId);
|
||||
console.log('API返回结果:', res);
|
||||
console.log('当前教师ID:', jsId);
|
||||
console.log('汇总API返回结果:', res);
|
||||
|
||||
if (res && res.rows && res.rows.length > 0) {
|
||||
// 查找当前教师的数据
|
||||
const teacherData = res.rows.find(row => row.id === teacherId);
|
||||
|
||||
if (!teacherData) {
|
||||
console.warn('未找到当前教师的积分数据');
|
||||
uni.showToast({
|
||||
title: '未找到积分数据',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('找到的教师积分数据:', teacherData);
|
||||
console.log('教师姓名:', teacherData.jsxm);
|
||||
console.log('总分:', teacherData.totalScore);
|
||||
console.log('积分汇总数据:', teacherData.summaries);
|
||||
|
||||
// 获取检查项数据
|
||||
const inspectItems = await loadInspectItems();
|
||||
|
||||
if (inspectItems.length === 0) {
|
||||
console.warn('未获取到检查项数据');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('检查项数据:', inspectItems);
|
||||
|
||||
// 构建评价项目数据,参考后端界面的getScore方法
|
||||
const items = [];
|
||||
let total = 0;
|
||||
|
||||
console.log('检查项数据:', inspectItems);
|
||||
console.log('教师积分汇总:', teacherData.summaries);
|
||||
|
||||
inspectItems.forEach((item, index) => {
|
||||
// 参考后端界面的getScore方法逻辑
|
||||
let score = 0;
|
||||
let num = 0;
|
||||
|
||||
if (teacherData.summaries && teacherData.summaries.length > 0) {
|
||||
for (let i = 0; i < teacherData.summaries.length; i++) {
|
||||
const summaryId = teacherData.summaries[i].inspectItemId;
|
||||
const itemId = item.id;
|
||||
|
||||
// 使用宽松比较,并考虑大小写
|
||||
if (summaryId == itemId || summaryId?.toUpperCase() == itemId?.toUpperCase()) {
|
||||
score = parseFloat(teacherData.summaries[i].score) || 0;
|
||||
num = parseInt(teacherData.summaries[i].num) || 0;
|
||||
console.log(`✅ 匹配成功: ${item.name}, score=${score}, num=${num}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: item.id,
|
||||
category: item.name,
|
||||
value: score,
|
||||
isUnderReview: num > 0, // 如果有记录数,说明在审核中
|
||||
num: num
|
||||
});
|
||||
|
||||
total += score;
|
||||
});
|
||||
const list = res?.result || res?.data || res || [];
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
const items: EvaluationItem[] = list.map((it: any) => ({
|
||||
id: it.jfTypeId,
|
||||
category: it.jfTypeName || '未命名',
|
||||
value: Number(it.totalScore || 0),
|
||||
isUnderReview: false // 汇总接口暂未提供审核状态
|
||||
}));
|
||||
|
||||
evaluationItems.value = items;
|
||||
totalScore.value = total;
|
||||
totalScore.value = items.reduce((sum, cur) => sum + (cur.value || 0), 0);
|
||||
|
||||
console.log('处理后的积分数据:', {
|
||||
items: items,
|
||||
totalScore: total,
|
||||
reviewingCount: reviewingCountComputed.value
|
||||
items,
|
||||
totalScore: totalScore.value
|
||||
});
|
||||
} else {
|
||||
console.warn('查询结果为空或格式不正确');
|
||||
@ -238,32 +153,40 @@ const loadTeacherScore = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
function handleItemClick(item: any) {
|
||||
// 检查该项积分是否为0
|
||||
if (!item.value || item.value === 0 || item.value === '0' || item.value === 0.0 || item.value === null || item.value === undefined) {
|
||||
uni.showToast({
|
||||
title: '积分为0',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
function handleItemClick(item: EvaluationItem) {
|
||||
// 如果有分值且分值大于0,跳转到查看详细记录页面
|
||||
if (item.value && item.value > 0) {
|
||||
// 从本地缓存获取当前教师ID
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
let jsId = '';
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
jsId = userData.jsData.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const url = `/pages/view/routine/JiFenPingJia/detail?inspectItemId=${item.id}`;
|
||||
const url = `/pages/view/routine/JiFenPingJia/jfself/MyScoreDetail?jfTypeId=${item.id}&jsId=${jsId}`;
|
||||
uni.navigateTo({
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
|
||||
function scgrry() {
|
||||
} else {
|
||||
// 没有分值或分值为0,跳转到新增界面
|
||||
const url = `/pages/view/routine/JiFenPingJia/jfself/apply?jfTypeId=${item.id}&score=${item.value || 0}`;
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/JiFenPingJia/PersonalHonor`,
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scgkkhj() {
|
||||
function handleIntegralApply() {
|
||||
// 跳转到积分申请页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/JiFenPingJia/PublicClassAwards`,
|
||||
url: `/pages/view/routine/JiFenPingJia/jfself/IntegralApply`,
|
||||
});
|
||||
}
|
||||
|
||||
@ -294,31 +217,6 @@ onMounted(() => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #4caf50; // Green color from the image
|
||||
margin-left: 8px;
|
||||
border-radius: 2px; // Slightly rounded corners
|
||||
}
|
||||
|
||||
.review-count {
|
||||
background-color: #fff2e8;
|
||||
color: #ff6b35;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
margin-left: 12px;
|
||||
border: 1px solid #ff6b35;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
764
src/pages/view/routine/JiFenPingJia/jfself/MyScoreDetail.vue
Normal file
764
src/pages/view/routine/JiFenPingJia/jfself/MyScoreDetail.vue
Normal file
@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="container">
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!scoreList.length" class="empty-container">
|
||||
<text class="empty-text">暂无积分记录</text>
|
||||
</view>
|
||||
<view v-else class="score-list">
|
||||
<view
|
||||
v-for="(item, index) in scoreList"
|
||||
:key="index"
|
||||
class="score-item"
|
||||
>
|
||||
<uni-card :is-shadow="false" is-full>
|
||||
<view class="item-header">
|
||||
<text class="item-title">{{ item.jfTypeName || '未命名' }}</text>
|
||||
<text class="item-score">+{{ item.score || 0 }}分</text>
|
||||
</view>
|
||||
|
||||
<view class="item-content">
|
||||
<!-- 积分标准区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">积分标准</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row-vertical" v-if="item.ruleStandard">
|
||||
<text class="field-label">检查标准:</text>
|
||||
<text class="field-value multiline-text">{{ item.ruleStandard }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row-vertical" v-if="item.scoreConfig">
|
||||
<text class="field-label">业绩积分:</text>
|
||||
<view class="score-config-wrapper">
|
||||
<view
|
||||
v-for="(category, catIdx) in formatScoreConfig(item.scoreConfig)"
|
||||
:key="catIdx"
|
||||
class="score-config-category"
|
||||
>
|
||||
<view v-if="category.category" class="category-title">{{ category.category }}</view>
|
||||
<!-- 单个分值显示:表格形式,包含"级别"和"分值"两列 -->
|
||||
<view v-if="!category.hasMultipleGrades" class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">分值</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ row.scores }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 多个分值显示:表格形式,将"一等奖、二等奖、三等奖"作为列 -->
|
||||
<view v-else class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">一等奖</view>
|
||||
<view class="table-cell header-cell">二等奖</view>
|
||||
<view class="table-cell header-cell">三等奖</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[0] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[1] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[2] || '') : '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 积分考核区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">获奖情况</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.jsxm">
|
||||
<text class="field-label">考核教师:</text>
|
||||
<text class="field-value">{{ item.jsxm }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.rymc">
|
||||
<text class="field-label">荣誉名称:</text>
|
||||
<text class="field-value">{{ item.rymc }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.hjlx">
|
||||
<text class="field-label">获奖类型:</text>
|
||||
<text class="field-value">{{ item.hjlx }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.bjdw">
|
||||
<text class="field-label">颁奖单位:</text>
|
||||
<text class="field-value">{{ item.bjdw }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.hjtime">
|
||||
<text class="field-label">获奖时间:</text>
|
||||
<text class="field-value">{{ item.hjtime }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.category">
|
||||
<text class="field-label">类别:</text>
|
||||
<text class="field-value">{{ item.category }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.level">
|
||||
<text class="field-label">级别:</text>
|
||||
<text class="field-value">{{ item.level }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.grade">
|
||||
<text class="field-label">等级:</text>
|
||||
<text class="field-value">{{ item.grade }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.score">
|
||||
<text class="field-label">分值:</text>
|
||||
<text class="field-value score-value">{{ item.score }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.khTime">
|
||||
<text class="field-label">评价时间:</text>
|
||||
<text class="field-value">{{ item.khTime }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.khjsxm">
|
||||
<text class="field-label">评价人:</text>
|
||||
<text class="field-value">{{ item.khjsxm }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.khRemark">
|
||||
<text class="field-label">备注:</text>
|
||||
<text class="field-value">{{ item.khRemark }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.jfStatus">
|
||||
<text class="field-label">状态:</text>
|
||||
<text class="field-value">{{ getStatusText(item.jfStatus) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field-row" v-if="item.fileName">
|
||||
<text class="field-label">附件:</text>
|
||||
<view class="file-wrapper">
|
||||
<text class="file-name" @click="previewFile(item)">{{ item.fileName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 固定在底部的返回按钮 -->
|
||||
<view v-if="scoreList.length > 0" class="fixed-bottom">
|
||||
<button class="back-btn" @click="goBack">
|
||||
返回
|
||||
</button>
|
||||
</view>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { jfFindPageApi, jfTypeStructureApi } from "@/api/base/server";
|
||||
|
||||
interface ScoreItem {
|
||||
id?: string;
|
||||
jfTypeId?: string;
|
||||
jfTypeName?: string;
|
||||
ruleStandard?: string;
|
||||
scoreConfig?: any;
|
||||
jsxm?: string;
|
||||
rymc?: string;
|
||||
hjlx?: string;
|
||||
bjdw?: string;
|
||||
hjtime?: string;
|
||||
category?: string;
|
||||
level?: string;
|
||||
grade?: string;
|
||||
score?: number;
|
||||
khTime?: string;
|
||||
khjsxm?: string;
|
||||
khRemark?: string;
|
||||
jfStatus?: string;
|
||||
fileName?: string;
|
||||
fileFormat?: string;
|
||||
fileUrl?: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const scoreList = ref<ScoreItem[]>([]);
|
||||
|
||||
// 默认级别配置
|
||||
const defaultLevels = [
|
||||
{ levelCode: 'national', levelName: '国', key: 'levelNational' },
|
||||
{ levelCode: 'province', levelName: '省', key: 'levelProvince' },
|
||||
{ levelCode: 'city', levelName: '市', key: 'levelCity' },
|
||||
{ levelCode: 'district', levelName: '区', key: 'levelDistrict' },
|
||||
{ levelCode: 'school_district', levelName: '学区/街道', key: 'levelSchoolDistrict' },
|
||||
{ levelCode: 'school', levelName: '校', key: 'levelSchool' },
|
||||
];
|
||||
|
||||
interface ScoreConfigRow {
|
||||
level: string;
|
||||
scores: string | number[]; // 单个分值用字符串,多个分值用数组
|
||||
hasMultipleGrades: boolean; // 是否有多个等级
|
||||
}
|
||||
|
||||
interface ScoreConfigCategory {
|
||||
category?: string;
|
||||
rows: ScoreConfigRow[];
|
||||
hasMultipleGrades: boolean; // 整个类别是否有多个等级
|
||||
}
|
||||
|
||||
// 将分值数组格式化为字符串
|
||||
// 如果只有一个分值:返回 "30"
|
||||
// 如果有多个分值:返回数组 [28, 26, 24]
|
||||
function formatScores(scores: number[]): { display: string | number[], hasMultiple: boolean } {
|
||||
if (!scores || scores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
const validScores = scores.filter(s => s > 0);
|
||||
if (validScores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
|
||||
// 如果只有一个分值,返回字符串
|
||||
if (validScores.length === 1) {
|
||||
return { display: String(validScores[0]), hasMultiple: false };
|
||||
}
|
||||
|
||||
// 如果有多个分值,返回数组
|
||||
return { display: validScores, hasMultiple: true };
|
||||
}
|
||||
|
||||
const formatScoreConfig = (config: any): ScoreConfigCategory[] => {
|
||||
if (!config) return [];
|
||||
try {
|
||||
let configObj = config;
|
||||
if (typeof config === "string") {
|
||||
try {
|
||||
configObj = JSON.parse(config);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const result: ScoreConfigCategory[] = [];
|
||||
|
||||
if (configObj.categories && Array.isArray(configObj.categories)) {
|
||||
configObj.categories.forEach((cat: any) => {
|
||||
const categoryName = cat.category || '';
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
defaultLevels.forEach(level => {
|
||||
const scores = cat[level.key] as number[];
|
||||
if (scores && scores.length > 0) {
|
||||
const formatted = formatScores(scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
rows.push({
|
||||
level: level.levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: categoryName,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (configObj.category && configObj.levels) {
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
configObj.levels.forEach((level: any) => {
|
||||
if (level.scores && level.scores.length > 0) {
|
||||
const formatted = formatScores(level.scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
const levelName = level.levelName || level.levelCode || '';
|
||||
rows.push({
|
||||
level: levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: configObj.category,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
'D': '已完结',
|
||||
'A': '待审核',
|
||||
'B': '审核中',
|
||||
'C': '已驳回'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
function previewFile(item: ScoreItem) {
|
||||
if (item.fileUrl) {
|
||||
uni.previewImage({
|
||||
urls: [item.fileUrl],
|
||||
fail: (err) => {
|
||||
console.error('预览文件失败:', err);
|
||||
uni.showToast({
|
||||
title: '预览文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 加载积分记录数据
|
||||
const loadScoreList = async (jfTypeId: string, jsId: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 从本地缓存获取当前教师ID
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
if (!userDataStr) {
|
||||
uni.showToast({
|
||||
title: '未找到用户数据',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let userData = null;
|
||||
try {
|
||||
userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (!userData || !userData.jsData || !userData.jsData.id) {
|
||||
uni.showToast({
|
||||
title: '未找到教师信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '用户数据解析失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先获取顶级节点下的所有子节点 ID(包括二级、三级等)
|
||||
const structureRes: any = await jfTypeStructureApi({
|
||||
topTypeId: jfTypeId
|
||||
});
|
||||
|
||||
const structureList = structureRes?.result || structureRes?.data || structureRes || [];
|
||||
// 提取所有节点 ID(包括顶级节点本身)
|
||||
const allJfTypeIds: string[] = [jfTypeId]; // 包含顶级节点
|
||||
structureList.forEach((item: any) => {
|
||||
if (item.jfTypeId && !allJfTypeIds.includes(item.jfTypeId)) {
|
||||
allJfTypeIds.push(item.jfTypeId);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('顶级节点ID:', jfTypeId);
|
||||
console.log('所有子节点ID:', allJfTypeIds);
|
||||
|
||||
// 2. 使用所有子节点 ID 查询积分记录
|
||||
const params = {
|
||||
jfTypeIds: allJfTypeIds.join(','), // 传递多个 jfTypeId,用逗号分隔
|
||||
jsId: jsId || userData.jsData.id,
|
||||
page: 1,
|
||||
rows: 1000 // 获取所有记录
|
||||
};
|
||||
|
||||
const res: any = await jfFindPageApi(params);
|
||||
console.log('查询参数:', params);
|
||||
console.log('积分记录API返回结果:', res);
|
||||
|
||||
const list = res?.result?.rows || res?.data?.rows || res?.rows || [];
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
scoreList.value = list.map((it: any) => ({
|
||||
id: it.id,
|
||||
jfTypeId: it.jfTypeId,
|
||||
jfTypeName: it.jfTypeName,
|
||||
ruleStandard: it.ruleStandard,
|
||||
scoreConfig: it.scoreConfig,
|
||||
jsxm: it.jsxm,
|
||||
rymc: it.rymc,
|
||||
hjlx: it.hjlx,
|
||||
bjdw: it.bjdw,
|
||||
hjtime: it.hjtime,
|
||||
category: it.category,
|
||||
level: it.level,
|
||||
grade: it.grade,
|
||||
score: it.score,
|
||||
khTime: it.khTime,
|
||||
khjsxm: it.khjsxm,
|
||||
khRemark: it.khRemark,
|
||||
jfStatus: it.jfStatus,
|
||||
fileName: it.fileName,
|
||||
fileFormat: it.fileFormat,
|
||||
fileUrl: it.fileUrl
|
||||
}));
|
||||
|
||||
console.log('处理后的积分记录数据:', scoreList.value);
|
||||
} else {
|
||||
console.warn('查询结果为空');
|
||||
scoreList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取积分记录失败:', error);
|
||||
uni.showToast({
|
||||
title: '获取积分记录失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
const jfTypeId = options?.jfTypeId || "";
|
||||
const jsId = options?.jsId || "";
|
||||
|
||||
if (!jfTypeId) {
|
||||
uni.showToast({
|
||||
title: "缺少积分类型ID",
|
||||
icon: "none"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadScoreList(jfTypeId, jsId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20rpx 20rpx 160rpx 20rpx; // 为底部固定按钮留出空间
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.score-list {
|
||||
.score-item {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.item-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-score {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #f56c6c;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
.section {
|
||||
margin-top: 24rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 0 12rpx;
|
||||
|
||||
.title-line {
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background: #1890ff;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.8;
|
||||
align-items: flex-start;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
min-width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
|
||||
&.score-value {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.multiline-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.file-wrapper {
|
||||
flex: 1;
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #1890ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-row-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
|
||||
&.multiline-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.score-config-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.score-config-wrapper {
|
||||
flex: 1;
|
||||
|
||||
.score-config-category {
|
||||
margin-top: 16rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
background-color: #ecf5ff;
|
||||
border-radius: 6rpx;
|
||||
border-left: 4rpx solid #409eff;
|
||||
}
|
||||
|
||||
// 分值表格样式(单个分值和多个分值共用)
|
||||
.score-config-table {
|
||||
border: 1rpx solid #dcdfe6;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1rpx solid #dcdfe6;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #ebeef5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 16rpx 12rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
|
||||
&:first-child {
|
||||
width: 120rpx;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
}
|
||||
|
||||
// 单个分值:只有两列(级别、分值)
|
||||
&:nth-child(2):last-child {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
// 多个分值:有四列(级别、一等奖、二等奖、三等奖)
|
||||
&:nth-child(2):not(:last-child),
|
||||
&:nth-child(3),
|
||||
&:nth-child(4) {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.header-cell {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.score-cell {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove default card padding
|
||||
::v-deep .uni-card .uni-card__content {
|
||||
padding: 20rpx !important;
|
||||
}
|
||||
|
||||
// 固定在底部的返回按钮
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.3);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
box-shadow: 0 6rpx 20rpx rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1193
src/pages/view/routine/JiFenPingJia/jfself/addjf.vue
Normal file
1193
src/pages/view/routine/JiFenPingJia/jfself/addjf.vue
Normal file
File diff suppressed because it is too large
Load Diff
652
src/pages/view/routine/JiFenPingJia/jfself/apply.vue
Normal file
652
src/pages/view/routine/JiFenPingJia/jfself/apply.vue
Normal file
@ -0,0 +1,652 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="apply-container">
|
||||
<view class="content-wrapper">
|
||||
<view v-if="loading" class="loading-text">加载中...</view>
|
||||
<view v-else>
|
||||
<view v-if="!treeData.length" class="loading-text">暂无数据</view>
|
||||
<view v-else class="tree-wrapper">
|
||||
<view v-for="node in treeData" :key="node.jfTypeId" class="tree-node level-1">
|
||||
<view class="node-header">
|
||||
<view class="node-title-wrapper">
|
||||
<text class="node-title">{{ node.jfTypeName }}</text>
|
||||
<text class="node-badge level-1-badge">一级</text>
|
||||
</view>
|
||||
<text class="node-score" v-if="!isZero && node.totalScore">分值:{{ node.totalScore }}</text>
|
||||
</view>
|
||||
<view class="node-content">
|
||||
<view class="node-field" v-if="node.ruleStandard">
|
||||
<view class="field-label-title">检查标准</view>
|
||||
<text class="field-value">{{ node.ruleStandard }}</text>
|
||||
</view>
|
||||
<view class="node-field" v-if="node.scoreConfig">
|
||||
<view class="field-label-title">业绩积分</view>
|
||||
<view v-for="(category, catIdx) in formatScoreConfig(node.scoreConfig)" :key="catIdx" class="score-config-category">
|
||||
<view v-if="category.category" class="category-title">{{ category.category }}</view>
|
||||
<!-- 单个分值显示:表格形式,包含"级别"和"分值"两列 -->
|
||||
<view v-if="!category.hasMultipleGrades" class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">分值</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ row.scores }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 多个分值显示:表格形式,将"一等奖、二等奖、三等奖"作为列 -->
|
||||
<view v-else class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">一等奖</view>
|
||||
<view class="table-cell header-cell">二等奖</view>
|
||||
<view class="table-cell header-cell">三等奖</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[0] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[1] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[2] || '') : '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="node.children && node.children.length" class="node-children">
|
||||
<view v-for="child in node.children" :key="child.jfTypeId" class="tree-node level-2">
|
||||
<view class="node-header">
|
||||
<view class="node-title-wrapper">
|
||||
<text class="node-title">{{ child.jfTypeName }}</text>
|
||||
<text class="node-badge level-2-badge">二级</text>
|
||||
</view>
|
||||
<text class="node-score" v-if="!isZero && child.totalScore">分值:{{ child.totalScore }}</text>
|
||||
</view>
|
||||
<view class="node-content">
|
||||
<view class="node-field" v-if="child.ruleStandard">
|
||||
<view class="field-label-title">检查标准</view>
|
||||
<text class="field-value">{{ child.ruleStandard }}</text>
|
||||
</view>
|
||||
<view class="node-field" v-if="child.scoreConfig">
|
||||
<view class="field-label-title">业绩积分</view>
|
||||
<view v-for="(category, catIdx) in formatScoreConfig(child.scoreConfig)" :key="catIdx" class="score-config-category">
|
||||
<view v-if="category.category" class="category-title">{{ category.category }}</view>
|
||||
<!-- 单个分值显示:表格形式,包含"级别"和"分值"两列 -->
|
||||
<view v-if="!category.hasMultipleGrades" class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">分值</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ row.scores }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 多个分值显示:表格形式,将"一等奖、二等奖、三等奖"作为列 -->
|
||||
<view v-else class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">一等奖</view>
|
||||
<view class="table-cell header-cell">二等奖</view>
|
||||
<view class="table-cell header-cell">三等奖</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[0] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[1] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[2] || '') : '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="child.children && child.children.length" class="node-children">
|
||||
<view v-for="g in child.children" :key="g.jfTypeId" class="tree-node level-3">
|
||||
<view class="node-header">
|
||||
<view class="node-title-wrapper">
|
||||
<text class="node-title">{{ g.jfTypeName }}</text>
|
||||
<text class="node-badge level-3-badge">三级</text>
|
||||
</view>
|
||||
<text class="node-score" v-if="!isZero && g.totalScore">分值:{{ g.totalScore }}</text>
|
||||
</view>
|
||||
<view class="node-content">
|
||||
<view class="node-field" v-if="g.ruleStandard">
|
||||
<view class="field-label-title">检查标准</view>
|
||||
<text class="field-value">{{ g.ruleStandard }}</text>
|
||||
</view>
|
||||
<view class="node-field" v-if="g.scoreConfig">
|
||||
<view class="field-label-title">业绩积分</view>
|
||||
<view v-for="(category, catIdx) in formatScoreConfig(g.scoreConfig)" :key="catIdx" class="score-config-category">
|
||||
<view v-if="category.category" class="category-title">{{ category.category }}</view>
|
||||
<!-- 单个分值显示:表格形式,包含"级别"和"分值"两列 -->
|
||||
<view v-if="!category.hasMultipleGrades" class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">分值</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ row.scores }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 多个分值显示:表格形式,将"一等奖、二等奖、三等奖"作为列 -->
|
||||
<view v-else class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">一等奖</view>
|
||||
<view class="table-cell header-cell">二等奖</view>
|
||||
<view class="table-cell header-cell">三等奖</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[0] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[1] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[2] || '') : '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 固定在底部的返回按钮 -->
|
||||
<view v-if="treeData.length > 0" class="fixed-bottom">
|
||||
<button class="back-btn" @click="goBack">
|
||||
返回
|
||||
</button>
|
||||
</view>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { jfTypeStructureApi, jfScoreStructureApi } from "@/api/base/server";
|
||||
|
||||
interface TreeItem {
|
||||
jfTypeId: string;
|
||||
pid?: string;
|
||||
jfTypeName: string;
|
||||
ruleStandard?: string;
|
||||
scoreConfig?: any;
|
||||
totalScore?: number;
|
||||
children?: TreeItem[];
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const isZero = ref(false);
|
||||
const treeData = ref<TreeItem[]>([]);
|
||||
|
||||
// 默认级别配置
|
||||
const defaultLevels = [
|
||||
{ levelCode: 'national', levelName: '国', key: 'levelNational' },
|
||||
{ levelCode: 'province', levelName: '省', key: 'levelProvince' },
|
||||
{ levelCode: 'city', levelName: '市', key: 'levelCity' },
|
||||
{ levelCode: 'district', levelName: '区', key: 'levelDistrict' },
|
||||
{ levelCode: 'school_district', levelName: '学区/街道', key: 'levelSchoolDistrict' },
|
||||
{ levelCode: 'school', levelName: '校', key: 'levelSchool' },
|
||||
];
|
||||
|
||||
interface ScoreConfigRow {
|
||||
level: string;
|
||||
scores: string | number[]; // 单个分值用字符串,多个分值用数组
|
||||
hasMultipleGrades: boolean; // 是否有多个等级
|
||||
}
|
||||
|
||||
interface ScoreConfigCategory {
|
||||
category?: string;
|
||||
rows: ScoreConfigRow[];
|
||||
hasMultipleGrades: boolean; // 整个类别是否有多个等级
|
||||
}
|
||||
|
||||
// 将分值数组格式化为字符串
|
||||
// 如果只有一个分值:返回 "30"
|
||||
// 如果有多个分值:返回数组 [28, 26, 24]
|
||||
function formatScores(scores: number[]): { display: string | number[], hasMultiple: boolean } {
|
||||
if (!scores || scores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
const validScores = scores.filter(s => s > 0);
|
||||
if (validScores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
|
||||
// 如果只有一个分值,返回字符串
|
||||
if (validScores.length === 1) {
|
||||
return { display: String(validScores[0]), hasMultiple: false };
|
||||
}
|
||||
|
||||
// 如果有多个分值,返回数组
|
||||
return { display: validScores, hasMultiple: true };
|
||||
}
|
||||
|
||||
const formatScoreConfig = (config: any): ScoreConfigCategory[] => {
|
||||
if (!config) return [];
|
||||
try {
|
||||
// 如果是字符串,尝试解析
|
||||
let configObj = config;
|
||||
if (typeof config === "string") {
|
||||
try {
|
||||
configObj = JSON.parse(config);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const result: ScoreConfigCategory[] = [];
|
||||
|
||||
// 如果已经有 categories 格式
|
||||
if (configObj.categories && Array.isArray(configObj.categories)) {
|
||||
configObj.categories.forEach((cat: any) => {
|
||||
const categoryName = cat.category || '';
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
defaultLevels.forEach(level => {
|
||||
const scores = cat[level.key] as number[];
|
||||
if (scores && scores.length > 0) {
|
||||
const formatted = formatScores(scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
rows.push({
|
||||
level: level.levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: categoryName,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// 如果有旧的 category + levels 格式
|
||||
else if (configObj.category && configObj.levels) {
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
configObj.levels.forEach((level: any) => {
|
||||
if (level.scores && level.scores.length > 0) {
|
||||
const formatted = formatScores(level.scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
const levelName = level.levelName || level.levelCode || '';
|
||||
rows.push({
|
||||
level: levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: configObj.category,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
function buildTree(list: any[]): TreeItem[] {
|
||||
const map: Record<string, TreeItem> = {};
|
||||
const roots: TreeItem[] = [];
|
||||
|
||||
// 先创建所有节点
|
||||
list.forEach(it => {
|
||||
map[it.jfTypeId] = {
|
||||
jfTypeId: it.jfTypeId,
|
||||
pid: it.pid,
|
||||
jfTypeName: it.jfTypeName,
|
||||
ruleStandard: it.ruleStandard,
|
||||
scoreConfig: it.scoreConfig,
|
||||
totalScore: it.totalScore
|
||||
};
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
list.forEach(it => {
|
||||
const node = map[it.jfTypeId];
|
||||
if (it.pid && map[it.pid]) {
|
||||
if (!map[it.pid].children) {
|
||||
map[it.pid].children = [];
|
||||
}
|
||||
map[it.pid].children!.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
const loadData = async (jfTypeId: string, score: number, jsId: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
topTypeId: jfTypeId,
|
||||
startTime: new Date().getFullYear() + "-01-01",
|
||||
endTime: new Date().getFullYear() + "-12-31",
|
||||
};
|
||||
let res: any;
|
||||
|
||||
if (score && score > 0) {
|
||||
// 积分不为0,显示教师获得积分信息
|
||||
params.jsId = jsId;
|
||||
res = await jfScoreStructureApi(params);
|
||||
} else {
|
||||
// 积分为0,显示向下每个维度的积分项目、检查标准、分值配置
|
||||
res = await jfTypeStructureApi(params);
|
||||
}
|
||||
|
||||
const list = res?.result || res?.data || res || [];
|
||||
treeData.value = buildTree(Array.isArray(list) ? list : []);
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载数据失败",
|
||||
icon: "none"
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
const jfTypeId = options?.jfTypeId || "";
|
||||
const score = Number(options?.score || 0);
|
||||
isZero.value = score <= 0;
|
||||
|
||||
let jsId = "";
|
||||
const userDataStr = uni.getStorageSync("app-user");
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = typeof userDataStr === "string" ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
jsId = userData.jsData.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析用户数据失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jfTypeId) {
|
||||
uni.showToast({ title: "缺少积分类型", icon: "none" });
|
||||
return;
|
||||
}
|
||||
loadData(jfTypeId, score, jsId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 20rpx 20rpx 160rpx 20rpx; // 为底部固定按钮留出空间
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.tree-wrapper {
|
||||
.tree-node {
|
||||
margin-bottom: 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&.level-1 {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
padding: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
&.level-3 {
|
||||
padding: 16rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.node-title-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.node-badge {
|
||||
display: inline-block;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: normal;
|
||||
|
||||
&.level-1-badge {
|
||||
background-color: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.level-2-badge {
|
||||
background-color: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.level-3-badge {
|
||||
background-color: #fdf6ec;
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
.node-score {
|
||||
font-size: 28rpx;
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.node-content {
|
||||
.node-field {
|
||||
margin-top: 20rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.field-label-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 12rpx;
|
||||
padding-bottom: 8rpx;
|
||||
border-bottom: 2rpx solid #e4e7ed;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.score-config-category {
|
||||
margin-top: 16rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
background-color: #ecf5ff;
|
||||
border-radius: 6rpx;
|
||||
border-left: 4rpx solid #409eff;
|
||||
}
|
||||
|
||||
// 分值表格样式(单个分值和多个分值共用)
|
||||
.score-config-table {
|
||||
border: 1rpx solid #dcdfe6;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1rpx solid #dcdfe6;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #ebeef5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 16rpx 12rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
|
||||
&:first-child {
|
||||
width: 120rpx;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
}
|
||||
|
||||
// 单个分值:只有两列(级别、分值)
|
||||
&:nth-child(2):last-child {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
// 多个分值:有四列(级别、一等奖、二等奖、三等奖)
|
||||
&:nth-child(2):not(:last-child),
|
||||
&:nth-child(3),
|
||||
&:nth-child(4) {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.header-cell {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.score-cell {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-children {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定在底部的返回按钮
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.3);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
box-shadow: 0 6rpx 20rpx rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
475
src/pages/view/routine/JiFenPingJia/jfsp/JfFlow.vue
Normal file
475
src/pages/view/routine/JiFenPingJia/jfsp/JfFlow.vue
Normal file
@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="p-15">
|
||||
<!-- 积分标准 -->
|
||||
<view class="jf-info-section">
|
||||
<view class="section-title">积分标准</view>
|
||||
<view class="info-item">
|
||||
<text class="label">积分项目:</text>
|
||||
<text class="value title-bold">{{ jfInfo.jfTypeName || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-block">
|
||||
<text class="block-label">检查标准</text>
|
||||
<text class="block-value multi-text">{{ jfInfo.ruleStandard || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-block">
|
||||
<text class="block-label">业绩积分</text>
|
||||
<view v-if="formattedScoreConfig.length" class="score-config-wrapper">
|
||||
<view
|
||||
v-for="(category, catIdx) in formattedScoreConfig"
|
||||
:key="catIdx"
|
||||
class="score-config-category"
|
||||
>
|
||||
<view v-if="category.category" class="category-title">{{ category.category }}</view>
|
||||
<view v-if="!category.hasMultipleGrades" class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">分值</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ row.scores }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="score-config-table">
|
||||
<view class="table-header">
|
||||
<view class="table-cell header-cell">级别</view>
|
||||
<view class="table-cell header-cell">一等奖</view>
|
||||
<view class="table-cell header-cell">二等奖</view>
|
||||
<view class="table-cell header-cell">三等奖</view>
|
||||
</view>
|
||||
<view v-for="(row, idx) in category.rows" :key="idx" class="table-row">
|
||||
<view class="table-cell">{{ row.level }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[0] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[1] || '') : '' }}</view>
|
||||
<view class="table-cell score-cell">{{ Array.isArray(row.scores) ? (row.scores[2] || '') : '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="block-value placeholder">暂无配置</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 获奖情况 -->
|
||||
<view class="jf-info-section">
|
||||
<view class="section-title">获奖情况</view>
|
||||
<view class="info-item">
|
||||
<text class="label">考核教师:</text>
|
||||
<text class="value title-bold">{{ jfInfo.jsmx || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">荣誉名称:</text>
|
||||
<text class="value">{{ jfInfo.rymc || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">获奖类型:</text>
|
||||
<text class="value">{{ jfInfo.hjlx || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">颁奖单位:</text>
|
||||
<text class="value multi-text">{{ jfInfo.bjdw || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">获奖时间:</text>
|
||||
<text class="value">{{ formatYearMonth(jfInfo.hjtime) }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">类别:</text>
|
||||
<text class="value">{{ jfInfo.category || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">级别:</text>
|
||||
<text class="value">{{ jfInfo.level || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="jfInfo.grade">
|
||||
<text class="label">等级:</text>
|
||||
<text class="value">{{ jfInfo.grade }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">分值:</text>
|
||||
<text class="value score-value">{{ jfInfo.score ?? '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 证书预览 -->
|
||||
<BasicFilePreview
|
||||
v-if="jfInfo.fileUrl"
|
||||
:file-url="jfInfo.fileUrl"
|
||||
:file-name="jfInfo.fileName"
|
||||
:file-format="jfInfo.fileFormat"
|
||||
class="mb-0"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 审批流程展示 -->
|
||||
<LcglSp :yw-id="jfId" yw-type="JF" />
|
||||
|
||||
<template #bottom>
|
||||
<YwConfirm
|
||||
v-if="showButton"
|
||||
:spApi="jfSpApi"
|
||||
:stopApi="jfStopApi"
|
||||
:transferApi="jfTransferApi"
|
||||
:params="spParams"
|
||||
:showXt="false"
|
||||
:showReject="true"
|
||||
:showTransfer="true"
|
||||
:showApprove="true"
|
||||
:showReturn="false"
|
||||
:showStop="true"
|
||||
:showXtDk="false"
|
||||
/>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref, computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import BasicLayout from "@/components/BasicLayout/Layout.vue";
|
||||
import LcglSp from "@/components/LcglSp/index.vue";
|
||||
import YwConfirm from "@/pages/components/YwConfirm/index.vue";
|
||||
import BasicFilePreview from "@/components/BasicFile/preview.vue";
|
||||
import { jfFlowByIdApi, jfSpApi, jfTransferApi, jfStopApi } from "@/api/base/server";
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
|
||||
const { getXxts } = useDataStore();
|
||||
|
||||
const jfId = ref<string>("");
|
||||
const jfInfo = ref<any>({});
|
||||
const showButton = ref<boolean>(false);
|
||||
|
||||
const spParams = computed(() => ({
|
||||
xxtsId: getXxts?.id,
|
||||
ywId: jfId.value
|
||||
}));
|
||||
|
||||
// 时间格式化
|
||||
const formatTime = (time: any) => {
|
||||
if (!time) return "-";
|
||||
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
|
||||
};
|
||||
|
||||
const formatYearMonth = (time: any) => {
|
||||
if (!time) return "-";
|
||||
return dayjs(time).format("YYYY-MM");
|
||||
};
|
||||
|
||||
// ===== 业绩积分展示格式化(与 MyScoreDetail 同步) =====
|
||||
const defaultLevels = [
|
||||
{ levelCode: 'national', levelName: '国', key: 'levelNational' },
|
||||
{ levelCode: 'province', levelName: '省', key: 'levelProvince' },
|
||||
{ levelCode: 'city', levelName: '市', key: 'levelCity' },
|
||||
{ levelCode: 'district', levelName: '区', key: 'levelDistrict' },
|
||||
{ levelCode: 'school_district', levelName: '学区/街道', key: 'levelSchoolDistrict' },
|
||||
{ levelCode: 'school', levelName: '校', key: 'levelSchool' },
|
||||
];
|
||||
|
||||
interface ScoreConfigRow {
|
||||
level: string;
|
||||
scores: string | number[]; // 单个分值用字符串,多个分值用数组
|
||||
hasMultipleGrades: boolean; // 是否有多个等级
|
||||
}
|
||||
|
||||
interface ScoreConfigCategory {
|
||||
category?: string;
|
||||
rows: ScoreConfigRow[];
|
||||
hasMultipleGrades: boolean; // 整个类别是否有多个等级
|
||||
}
|
||||
|
||||
const formatScores = (scores: number[]): { display: string | number[], hasMultiple: boolean } => {
|
||||
if (!scores || scores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
const validScores = scores.filter(s => s > 0);
|
||||
if (validScores.length === 0) {
|
||||
return { display: '', hasMultiple: false };
|
||||
}
|
||||
if (validScores.length === 1) {
|
||||
return { display: String(validScores[0]), hasMultiple: false };
|
||||
}
|
||||
return { display: validScores, hasMultiple: true };
|
||||
};
|
||||
|
||||
const formatScoreConfig = (config: any): ScoreConfigCategory[] => {
|
||||
if (!config) return [];
|
||||
try {
|
||||
let configObj = config;
|
||||
if (typeof config === "string") {
|
||||
try {
|
||||
configObj = JSON.parse(config);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const result: ScoreConfigCategory[] = [];
|
||||
|
||||
if (configObj.categories && Array.isArray(configObj.categories)) {
|
||||
configObj.categories.forEach((cat: any) => {
|
||||
const categoryName = cat.category || '';
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
defaultLevels.forEach(level => {
|
||||
const scores = cat[level.key] as number[];
|
||||
if (scores && scores.length > 0) {
|
||||
const formatted = formatScores(scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
rows.push({
|
||||
level: level.levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: categoryName,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (configObj.category && configObj.levels) {
|
||||
const rows: ScoreConfigRow[] = [];
|
||||
let categoryHasMultiple = false;
|
||||
|
||||
configObj.levels.forEach((level: any) => {
|
||||
if (level.scores && level.scores.length > 0) {
|
||||
const formatted = formatScores(level.scores);
|
||||
if (formatted.display) {
|
||||
if (formatted.hasMultiple) {
|
||||
categoryHasMultiple = true;
|
||||
}
|
||||
const levelName = level.levelName || level.levelCode || '';
|
||||
rows.push({
|
||||
level: levelName,
|
||||
scores: formatted.display,
|
||||
hasMultipleGrades: formatted.hasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.push({
|
||||
category: configObj.category,
|
||||
rows: rows,
|
||||
hasMultipleGrades: categoryHasMultiple
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const formattedScoreConfig = computed(() => formatScoreConfig(jfInfo.value?.scoreConfig));
|
||||
|
||||
// 读取流程详情
|
||||
const getJfInfo = async () => {
|
||||
try {
|
||||
const res: any = await jfFlowByIdApi({ id: jfId.value });
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
jfInfo.value = res.result.jfInfo || {};
|
||||
const spResult = jfInfo.value?.spResult;
|
||||
// 当流程已完成且待办状态关闭时,不显示审批按钮
|
||||
if (spResult && spResult !== "A" && getXxts && getXxts.dbZt === "A") {
|
||||
showButton.value = false;
|
||||
} else {
|
||||
showButton.value = true;
|
||||
}
|
||||
} else {
|
||||
showButton.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取积分流程失败:", error);
|
||||
showButton.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options && options.id) {
|
||||
jfId.value = options.id;
|
||||
getJfInfo();
|
||||
} else {
|
||||
uni.showToast({ title: "缺少积分ID", icon: "none" });
|
||||
setTimeout(() => uni.navigateBack(), 1500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.p-15 {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.jf-info-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007aff;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.label {
|
||||
width: 110rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
|
||||
&.title-bold {
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
&.score-value {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-block {
|
||||
margin-top: 12px;
|
||||
|
||||
.block-label {
|
||||
display: inline-block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
padding-bottom: 6rpx;
|
||||
border-bottom: 2rpx solid #e4e7ed;
|
||||
}
|
||||
|
||||
.block-value {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
|
||||
&.multi-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.score-config-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.score-config-category {
|
||||
margin-top: 16rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
background-color: #ecf5ff;
|
||||
border-radius: 6rpx;
|
||||
border-left: 4rpx solid #409eff;
|
||||
}
|
||||
|
||||
.score-config-table {
|
||||
border: 1rpx solid #dcdfe6;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1rpx solid #dcdfe6;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #ebeef5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 16rpx 12rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
|
||||
&:first-child {
|
||||
width: 120rpx;
|
||||
border-right: 1rpx solid #ebeef5;
|
||||
}
|
||||
|
||||
&:nth-child(2):last-child {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:nth-child(2):not(:last-child),
|
||||
&:nth-child(3),
|
||||
&:nth-child(4) {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.header-cell {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.score-cell {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
406
src/pages/view/routine/JiFenPingJia/jfsp/index.vue
Normal file
406
src/pages/view/routine/JiFenPingJia/jfsp/index.vue
Normal file
@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<view class="jfsp-list-page">
|
||||
<view class="top-section">
|
||||
<view class="search-card">
|
||||
<view class="search-item">
|
||||
<view class="search-container">
|
||||
<BasicSearch
|
||||
placeholder="搜索积分标题或类型"
|
||||
v-model="searchKeyword"
|
||||
class="search-input"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<u-button
|
||||
text="查询"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="search-button"
|
||||
@click="handleSearch(searchKeyword)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="tab in filterTabs"
|
||||
:key="tab.key"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="middle-section">
|
||||
<z-paging
|
||||
ref="pagingRef"
|
||||
v-model="dataList"
|
||||
:auto="true"
|
||||
:refresher-enabled="true"
|
||||
:loading-more-enabled="true"
|
||||
:loading-more-threshold="50"
|
||||
:default-page-size="10"
|
||||
:show-loading-more-no-more-view="true"
|
||||
:show-empty-view-reload="false"
|
||||
:fixed="false"
|
||||
:use-page-scroll="true"
|
||||
class="paging-container"
|
||||
@query="queryData"
|
||||
>
|
||||
<view
|
||||
v-for="(data, index) in dataList"
|
||||
:key="data.id || data.ywId || index"
|
||||
class="jf-card"
|
||||
>
|
||||
<view class="card-header">
|
||||
<text class="jf-title">{{ getTitle(data) }}</text>
|
||||
<text class="jf-status" :class="getStatusClass(data.spJd || data.dbZt)">
|
||||
{{ getStatusText(data.spJd || data.dbZt) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-item">
|
||||
<text class="info-label">考核教师:</text>
|
||||
<text class="info-value">{{ data.jsmx || "—" }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">获奖类型:</text>
|
||||
<text class="info-value">{{ data.hjlx || "—" }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">颁奖单位:</text>
|
||||
<text class="info-value multi-line">{{ data.bjdw || "—" }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">获奖时间:</text>
|
||||
<text class="info-value">{{ formatYearMonth(data.hjtime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="footer-actions">
|
||||
<u-button
|
||||
text="查看"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="goToDetail(data)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/Search.vue";
|
||||
import { jfFindUserTodosPageApi } from "@/api/base/server";
|
||||
import { navigateTo } from "@/utils/uniapp";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const filterTabs = [
|
||||
{ key: "pending", label: "待办" },
|
||||
{ key: "approved", label: "已办" },
|
||||
{ key: "cc", label: "抄送" },
|
||||
{ key: "all", label: "全部" },
|
||||
];
|
||||
|
||||
const activeTab = ref("pending");
|
||||
const searchKeyword = ref("");
|
||||
const dataList = ref<any[]>([]);
|
||||
const pagingRef = ref<any>(null);
|
||||
const userStore = useUserStore();
|
||||
|
||||
const getCurrentTeacherId = () => {
|
||||
const jsData = userStore.getJs;
|
||||
return jsData?.id || null;
|
||||
};
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
searchKeyword.value = keyword;
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
const switchTab = (tabKey: string) => {
|
||||
activeTab.value = tabKey;
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
const filterJfData = (list: any[]) => {
|
||||
let filtered = list;
|
||||
if (searchKeyword.value) {
|
||||
const kw = searchKeyword.value.toLowerCase();
|
||||
filtered = filtered.filter((item) =>
|
||||
getTitle(item).toLowerCase().includes(kw)
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const queryData = async (pageNo: number, pageSize: number) => {
|
||||
try {
|
||||
const jsId = getCurrentTeacherId();
|
||||
if (!jsId) {
|
||||
uni.showToast({ title: "无法获取用户信息", icon: "none" });
|
||||
pagingRef.value?.complete([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let response: any;
|
||||
if (activeTab.value === "all") {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "", jsId, spType: "", page: pageNo, rows: pageSize });
|
||||
} else if (activeTab.value === "pending") {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "A", jsId, spType: "SP", page: pageNo, rows: pageSize });
|
||||
} else if (activeTab.value === "approved") {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "B", jsId, spType: "SP", page: pageNo, rows: pageSize });
|
||||
} else {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "", jsId, spType: "CC", page: pageNo, rows: pageSize });
|
||||
}
|
||||
|
||||
console.log("积分审批列表接口返回 raw:", response);
|
||||
const result = response?.data || response;
|
||||
let rows: any[] = [];
|
||||
if (result?.rows && Array.isArray(result.rows)) {
|
||||
rows = result.rows;
|
||||
} else if (result?.resultCode === 1 && Array.isArray(result.result)) {
|
||||
rows = result.result;
|
||||
}
|
||||
console.log("积分审批列表 rows:", rows);
|
||||
const filtered = filterJfData(rows);
|
||||
console.log("积分审批列表 filtered:", filtered);
|
||||
// 直接写入 dataList,确保渲染
|
||||
dataList.value = filtered;
|
||||
// 将总数传递给 z-paging,优先使用 total/records
|
||||
const total = result?.total ?? result?.records ?? filtered.length;
|
||||
pagingRef.value?.complete(filtered, total);
|
||||
} catch (error) {
|
||||
console.error("加载积分审批列表失败:", error);
|
||||
uni.showToast({ title: "加载失败", icon: "none" });
|
||||
dataList.value = [];
|
||||
pagingRef.value?.complete([], 0);
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = (item: any) => item.ywTitle || item.title || item.rymc || "积分申请";
|
||||
|
||||
const getStatusText = (spJd?: string) => {
|
||||
const map: Record<string, string> = { A: "审批中", Z: "已完成", B: "已办", C: "已驳回" };
|
||||
return map[spJd || ""] || "未知";
|
||||
};
|
||||
|
||||
const getStatusClass = (spJd?: string) => {
|
||||
if (spJd === "A") return "status-pending";
|
||||
if (spJd === "Z" || spJd === "B") return "status-done";
|
||||
if (spJd === "C") return "status-reject";
|
||||
return "";
|
||||
};
|
||||
|
||||
const formatDate = (val?: string) => (val ? dayjs(val).format("YYYY-MM-DD HH:mm") : "--");
|
||||
const formatYearMonth = (val?: string) => (val ? dayjs(val).format("YYYY-MM") : "--");
|
||||
|
||||
const goToDetail = (item: any) => {
|
||||
const id = item.ywId || item.id;
|
||||
if (!id) {
|
||||
uni.showToast({ title: "缺少业务ID", icon: "none" });
|
||||
return;
|
||||
}
|
||||
navigateTo(`/pages/view/routine/JiFenPingJia/jfsp/JfFlow?id=${id}&from=db`);
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
pagingRef.value?.reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.jfsp-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.top-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 70%;
|
||||
height: 42px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 0 15px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
&.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
border-color: #007aff;
|
||||
}
|
||||
}
|
||||
|
||||
.middle-section {
|
||||
flex: 1;
|
||||
margin-top: 140px;
|
||||
height: calc(100vh - 140px);
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.paging-container {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.jf-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jf-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.jf-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff7e6;
|
||||
color: #f59e0b;
|
||||
}
|
||||
.status-done {
|
||||
background: #e8fff3;
|
||||
color: #10b981;
|
||||
}
|
||||
.status-reject {
|
||||
background: #ffecec;
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
|
||||
&.multi-line {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -197,7 +197,9 @@ const currentFilter = ref('all');
|
||||
|
||||
const totalCount = computed(() => teacherList.value.length);
|
||||
const signedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '1').length);
|
||||
const unsignedCount = computed(() => teacherList.value.filter(t => t.qdStatus === '0').length);
|
||||
const unsignedCount = computed(() => teacherList.value.filter(
|
||||
t => t.qdStatus === '0' && !(t.qjlx && t.qjlx.trim() !== '')
|
||||
).length);
|
||||
const lateCount = computed(() => {
|
||||
if (!qdInfo.value.qdkstime) return 0;
|
||||
const meetingStartTime = new Date(qdInfo.value.qdkstime);
|
||||
@ -227,7 +229,9 @@ const filteredTeacherList = computed(() => {
|
||||
return signInTime > meetingStartTime;
|
||||
});
|
||||
case 'unsigned':
|
||||
return teacherList.value.filter(t => t.qdStatus === '0');
|
||||
return teacherList.value.filter(
|
||||
t => t.qdStatus === '0' && !(t.qjlx && t.qjlx.trim() !== '')
|
||||
);
|
||||
case 'qj':
|
||||
return teacherList.value.filter(t => t.qjlx && t.qjlx.trim() !== '');
|
||||
case 'qj_pending':
|
||||
|
||||
@ -62,20 +62,11 @@
|
||||
<text class="deco-icon">🎁</text>
|
||||
<text class="deco-icon">🎈</text>
|
||||
</view>
|
||||
|
||||
<!-- 右上角音乐图标 -->
|
||||
<view
|
||||
class="music-icon-wrapper"
|
||||
:class="{ 'playing': isMusicPlaying, 'fade-out': isOpening }"
|
||||
@click="toggleMusic"
|
||||
>
|
||||
<text class="music-icon-emoji">🎵</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
@ -85,8 +76,6 @@ const openId = ref<string>("");
|
||||
const receiverName = ref<string>("");
|
||||
const isOpening = ref(false);
|
||||
const isOpened = ref(false);
|
||||
const audioContext = ref<any>(null);
|
||||
const isMusicPlaying = ref(false);
|
||||
|
||||
// 页面背景样式
|
||||
const pageStyle = computed(() => {
|
||||
@ -99,11 +88,6 @@ const pageStyle = computed(() => {
|
||||
const openEnvelope = () => {
|
||||
if (isOpening.value || isOpened.value) return;
|
||||
|
||||
// 用户点击时触发音乐播放(解决自动播放限制)
|
||||
if (!isMusicPlaying.value && audioContext.value) {
|
||||
playMusic();
|
||||
}
|
||||
|
||||
isOpening.value = true;
|
||||
|
||||
// 信封打开动画完成后跳转
|
||||
@ -118,124 +102,6 @@ const openEnvelope = () => {
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// 初始化音频(等待用户交互后自动播放)
|
||||
const initAudio = () => {
|
||||
try {
|
||||
// #ifdef H5
|
||||
audioContext.value = new Audio('/static/base/music/srkl.mp3');
|
||||
audioContext.value.loop = true;
|
||||
// 监听播放状态
|
||||
audioContext.value.addEventListener('play', () => {
|
||||
isMusicPlaying.value = true;
|
||||
});
|
||||
audioContext.value.addEventListener('pause', () => {
|
||||
isMusicPlaying.value = false;
|
||||
});
|
||||
audioContext.value.addEventListener('ended', () => {
|
||||
isMusicPlaying.value = false;
|
||||
});
|
||||
|
||||
// 标记是否已尝试播放
|
||||
let hasTriedPlay = false;
|
||||
|
||||
// 播放音乐的辅助函数
|
||||
const attemptPlay = () => {
|
||||
if (!audioContext.value || hasTriedPlay || isMusicPlaying.value) {
|
||||
return;
|
||||
}
|
||||
hasTriedPlay = true;
|
||||
audioContext.value.play().then(() => {
|
||||
isMusicPlaying.value = true;
|
||||
}).catch(() => {
|
||||
// 播放失败,重置标记以便下次尝试
|
||||
hasTriedPlay = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 监听用户交互事件,在第一次交互时自动播放
|
||||
let hasInteracted = false;
|
||||
const playOnInteraction = () => {
|
||||
if (!hasInteracted && !isMusicPlaying.value) {
|
||||
hasInteracted = true;
|
||||
attemptPlay();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听多种用户交互事件(只绑定一次)
|
||||
const events = ['click', 'touchstart'];
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, playOnInteraction, { once: true, passive: true });
|
||||
});
|
||||
|
||||
// 尝试自动播放(可能被阻止,但不记录错误)
|
||||
attemptPlay();
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
audioContext.value = uni.createInnerAudioContext();
|
||||
audioContext.value.src = '/static/base/music/srkl.mp3';
|
||||
audioContext.value.loop = true;
|
||||
// 监听播放状态
|
||||
audioContext.value.onPlay(() => {
|
||||
isMusicPlaying.value = true;
|
||||
});
|
||||
audioContext.value.onPause(() => {
|
||||
isMusicPlaying.value = false;
|
||||
});
|
||||
audioContext.value.onStop(() => {
|
||||
isMusicPlaying.value = false;
|
||||
});
|
||||
// 微信小程序可以自动播放
|
||||
audioContext.value.play();
|
||||
isMusicPlaying.value = true;
|
||||
// #endif
|
||||
} catch (e) {
|
||||
console.error("初始化音频失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 播放音乐
|
||||
const playMusic = () => {
|
||||
if (!audioContext.value) return;
|
||||
try {
|
||||
audioContext.value.play();
|
||||
isMusicPlaying.value = true;
|
||||
} catch (e) {
|
||||
console.error("播放音乐失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换音乐播放
|
||||
const toggleMusic = () => {
|
||||
if (!audioContext.value) return;
|
||||
|
||||
try {
|
||||
if (isMusicPlaying.value) {
|
||||
audioContext.value.pause();
|
||||
isMusicPlaying.value = false;
|
||||
} else {
|
||||
audioContext.value.play();
|
||||
isMusicPlaying.value = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("音乐播放控制失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面卸载时清理音频
|
||||
onUnmounted(() => {
|
||||
if (audioContext.value) {
|
||||
try {
|
||||
audioContext.value.pause();
|
||||
// #ifdef MP-WEIXIN
|
||||
audioContext.value.destroy?.();
|
||||
// #endif
|
||||
} catch (e) {
|
||||
console.error("清理音频失败:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 重置信封状态(返回时恢复初始状态)
|
||||
const resetEnvelopeState = () => {
|
||||
isOpening.value = false;
|
||||
@ -260,9 +126,6 @@ onLoad(async (options) => {
|
||||
if (!xxtsId.value) {
|
||||
uni.showToast({ title: "缺少贺卡ID", icon: "none" });
|
||||
}
|
||||
|
||||
// 初始化并播放背景音乐
|
||||
initAudio();
|
||||
});
|
||||
|
||||
// 页面显示时重置状态(从其他页面返回时)
|
||||
@ -613,46 +476,4 @@ onShow(() => {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
/* 右上角音乐图标 */
|
||||
.music-icon-wrapper {
|
||||
position: fixed;
|
||||
top: calc(20px + env(safe-area-inset-top));
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.music-icon-wrapper:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.music-icon-wrapper.playing .music-icon-emoji {
|
||||
animation: music-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.music-icon-wrapper.fade-out {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.music-icon-emoji {
|
||||
font-size: 28px;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes music-rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
471
src/pages/view/routine/sr/list.vue
Normal file
471
src/pages/view/routine/sr/list.vue
Normal file
@ -0,0 +1,471 @@
|
||||
<!-- src/pages/view/routine/sr/list.vue -->
|
||||
<!-- 生日清单页面(教师端) -->
|
||||
<template>
|
||||
<view class="birthday-list-page">
|
||||
<!-- 顶部标题 -->
|
||||
<view class="page-header">
|
||||
<view class="header-title">
|
||||
<text class="title-icon">🎂</text>
|
||||
<text class="title-text">生日清单</text>
|
||||
<text class="title-icon">🎂</text>
|
||||
</view>
|
||||
<!-- 日期范围选择器 -->
|
||||
<view class="date-range-picker">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="startDate"
|
||||
:start="minDate"
|
||||
:end="maxDate"
|
||||
@change="onStartDateChange"
|
||||
>
|
||||
<view class="date-input">{{ startDate }}</view>
|
||||
</picker>
|
||||
<text class="date-separator">~</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="endDate"
|
||||
:start="startDate"
|
||||
:end="maxDate"
|
||||
@change="onEndDateChange"
|
||||
>
|
||||
<view class="date-input">{{ endDate }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="isLoading" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<view v-else-if="birthdayList.length > 0" class="list-container">
|
||||
<!-- 教师生日 -->
|
||||
<view v-if="teacherList.length > 0" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-icon">👨🏫</text>
|
||||
<text class="section-title">教师生日</text>
|
||||
<text class="section-count">({{ teacherList.length }})</text>
|
||||
</view>
|
||||
<view class="list-items">
|
||||
<view
|
||||
v-for="(item, index) in teacherList"
|
||||
:key="index"
|
||||
class="list-item"
|
||||
@click="viewCard(item)"
|
||||
>
|
||||
<view class="item-avatar">
|
||||
<text class="avatar-icon">👨🏫</text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-name">{{ item.srPersonName }}</view>
|
||||
<view class="item-info">
|
||||
<text v-if="item.bc" class="info-text">{{ item.bc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-action">
|
||||
<text class="action-icon">🎁</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学生生日 -->
|
||||
<view v-if="studentList.length > 0" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-icon">👨🎓</text>
|
||||
<text class="section-title">学生生日</text>
|
||||
<text class="section-count">({{ studentList.length }})</text>
|
||||
</view>
|
||||
<view class="list-items">
|
||||
<view
|
||||
v-for="(item, index) in studentList"
|
||||
:key="index"
|
||||
class="list-item"
|
||||
@click="viewCard(item)"
|
||||
>
|
||||
<view class="item-avatar student-avatar">
|
||||
<text class="avatar-icon">👨🎓</text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-name">{{ item.srPersonName }}</view>
|
||||
<view class="item-info">
|
||||
<text v-if="item.bc" class="info-text">{{ item.bc }}</text>
|
||||
<text v-if="item.receiverName" class="info-text">家长:{{ item.receiverName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-action">
|
||||
<text class="action-icon">🎁</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="empty-container">
|
||||
<text class="empty-icon">🎂</text>
|
||||
<text class="empty-text">该日期范围内没有人生日哦~</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { onLoad, onPullDownRefresh } from "@dcloudio/uni-app";
|
||||
import { srFindPageApi } from "@/api/base/srApi";
|
||||
|
||||
// 数据
|
||||
const isLoading = ref(false);
|
||||
const birthdayList = ref<any[]>([]);
|
||||
|
||||
// 日期范围
|
||||
const getTodayStr = () => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const startDate = ref<string>(getTodayStr());
|
||||
const endDate = ref<string>(getTodayStr());
|
||||
|
||||
// 日期选择器的限制
|
||||
const minDate = ref<string>('2020-01-01');
|
||||
const maxDate = computed(() => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
});
|
||||
|
||||
// 计算属性:教师列表
|
||||
const teacherList = computed(() => {
|
||||
return birthdayList.value.filter(item => item.srPersonType === 'JS');
|
||||
});
|
||||
|
||||
// 计算属性:学生列表
|
||||
const studentList = computed(() => {
|
||||
return birthdayList.value.filter(item => item.srPersonType === 'XS');
|
||||
});
|
||||
|
||||
// 开始日期变化
|
||||
const onStartDateChange = (e: any) => {
|
||||
const date = e.detail.value;
|
||||
startDate.value = date;
|
||||
// 如果开始日期大于结束日期,自动调整结束日期
|
||||
if (date > endDate.value) {
|
||||
endDate.value = date;
|
||||
}
|
||||
// 重新查询
|
||||
fetchBirthdayList();
|
||||
};
|
||||
|
||||
// 结束日期变化
|
||||
const onEndDateChange = (e: any) => {
|
||||
const date = e.detail.value;
|
||||
endDate.value = date;
|
||||
// 如果结束日期小于开始日期,自动调整开始日期
|
||||
if (date < startDate.value) {
|
||||
startDate.value = date;
|
||||
}
|
||||
// 重新查询
|
||||
fetchBirthdayList();
|
||||
};
|
||||
|
||||
// 获取生日记录(根据日期范围)
|
||||
const fetchBirthdayList = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// 使用选择的日期范围查询
|
||||
const params: any = {
|
||||
page: 1,
|
||||
rows: 1000, // 获取足够多的数据
|
||||
birthdayDateStart: startDate.value, // 开始日期
|
||||
birthdayDateEnd: endDate.value, // 结束日期
|
||||
};
|
||||
|
||||
const res = await srFindPageApi(params);
|
||||
// 后端返回的数据结构:{total, page, records, rows: [...]}
|
||||
// records 是总记录数,rows 才是数据数组
|
||||
if (res && res.rows && Array.isArray(res.rows) && res.rows.length > 0) {
|
||||
// 去重:同一个过生日人只显示一次
|
||||
const uniqueMap = new Map();
|
||||
res.rows.forEach((item: any) => {
|
||||
const key = `${item.srPersonType}_${item.srPersonId}`;
|
||||
if (!uniqueMap.has(key)) {
|
||||
uniqueMap.set(key, item);
|
||||
}
|
||||
});
|
||||
birthdayList.value = Array.from(uniqueMap.values());
|
||||
} else {
|
||||
birthdayList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取生日清单失败:', error);
|
||||
uni.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
});
|
||||
birthdayList.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看贺卡
|
||||
const viewCard = (item: any) => {
|
||||
if (!item.id) {
|
||||
uni.showToast({
|
||||
title: '无法查看贺卡',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接跳转到新的贺卡查看页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/sr/viewCard?recordId=${item.id}&receiverId=${item.receiverId || ''}`
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
fetchBirthdayList();
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(() => {
|
||||
fetchBirthdayList().finally(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.birthday-list-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 60rpx 40rpx 40rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 48rpx;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 日期范围选择器 */
|
||||
.date-range-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
min-width: 200rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.date-input:active {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 0 20rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.list-items {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
|
||||
.item-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 30rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.student-avatar {
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-action {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
</style>
|
||||
|
||||
688
src/pages/view/routine/sr/viewCard.vue
Normal file
688
src/pages/view/routine/sr/viewCard.vue
Normal file
@ -0,0 +1,688 @@
|
||||
<!-- src/pages/view/routine/sr/viewCard.vue -->
|
||||
<!-- 生日贺卡查看页面(从清单页面进入) -->
|
||||
<template>
|
||||
<view class="card-view-page" :class="bgColorClass">
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="isLoading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 贺卡内容 -->
|
||||
<view v-else-if="cardData" class="card-container">
|
||||
<!-- 信纸主体 -->
|
||||
<view class="letter-paper">
|
||||
<!-- 信纸顶部装饰 -->
|
||||
<view class="paper-header">
|
||||
<view class="header-line"></view>
|
||||
<view class="header-title">
|
||||
<text class="title-icon">🎂</text>
|
||||
<text class="title-text">生日祝福</text>
|
||||
<text class="title-icon">🎂</text>
|
||||
</view>
|
||||
<view class="header-date">{{ formatBirthdayDate }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 称呼 -->
|
||||
<view class="letter-greeting" v-if="greetingFromContent">
|
||||
<view class="greeting-line">
|
||||
<text class="greeting-text">{{ greetingFromContent }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信件正文(带虚线) -->
|
||||
<view class="letter-content">
|
||||
<view
|
||||
v-for="(line, index) in contentLines"
|
||||
:key="index"
|
||||
class="content-line"
|
||||
>
|
||||
<text class="line-text">{{ line || '\u00A0' }}</text>
|
||||
<view class="line-border"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签名区域 -->
|
||||
<view class="letter-signature" v-if="cardData.signerName || cardData.signLabel">
|
||||
<view class="signature-wrapper">
|
||||
<!-- 签名文字 -->
|
||||
<view class="signature-info">
|
||||
<text class="signer-name">
|
||||
<text v-if="cardData.signLabel" class="signer-label">{{ cardData.signLabel }}</text>
|
||||
<text v-if="cardData.signerName">{{ cardData.signerName }}</text>
|
||||
</text>
|
||||
</view>
|
||||
<!-- 日期 -->
|
||||
<view class="signature-date">
|
||||
<text>{{ currentDate }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信纸底部装饰 -->
|
||||
<view class="paper-footer">
|
||||
<view class="footer-decoration">
|
||||
<text class="footer-icon">🎉</text>
|
||||
<text class="footer-icon">🎁</text>
|
||||
<text class="footer-icon">🎈</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右上角音乐图标 -->
|
||||
<view
|
||||
class="music-icon-wrapper"
|
||||
:class="{ 'playing': isMusicPlaying }"
|
||||
@click="toggleMusic"
|
||||
>
|
||||
<text class="music-icon-emoji">🎵</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-icon">🎂</text>
|
||||
<text class="empty-text">贺卡不存在或已过期</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { srGetCardDetailByRecordApi, srMarkAsViewedApi } from "@/api/base/srApi";
|
||||
import { imagUrl } from "@/utils";
|
||||
|
||||
const recordId = ref<string>("");
|
||||
const receiverId = ref<string>("");
|
||||
const cardData = ref<any>(null);
|
||||
const isLoading = ref(false);
|
||||
const isMusicPlaying = ref(false);
|
||||
const audioContext = ref<any>(null);
|
||||
|
||||
// 格式化生日日期
|
||||
const formatBirthdayDate = computed(() => {
|
||||
if (!cardData.value?.birthdayDate) return '';
|
||||
const date = cardData.value.birthdayDate;
|
||||
if (date.includes('月')) return date;
|
||||
try {
|
||||
const d = new Date(date);
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日`;
|
||||
} catch {
|
||||
return date;
|
||||
}
|
||||
});
|
||||
|
||||
// 当前日期
|
||||
const currentDate = computed(() => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
|
||||
});
|
||||
|
||||
// 背景颜色类名(根据后台配置)
|
||||
const bgColorClass = computed(() => {
|
||||
const colorType = cardData.value?.hkMb?.backgroundColor || cardData.value?.hkMb?.animationType || 'blue';
|
||||
return `bg-${colorType}`;
|
||||
});
|
||||
|
||||
// 去掉HTML标签,保留文本内容
|
||||
const stripHtmlTags = (html: string): string => {
|
||||
if (!html) return '';
|
||||
return html.replace(/<[^>]+>/g, '').trim();
|
||||
};
|
||||
|
||||
// 从内容中提取第一行作为称呼
|
||||
const greetingFromContent = computed(() => {
|
||||
if (!cardData.value?.zfContent) return '';
|
||||
const content = cardData.value.zfContent;
|
||||
const lines = content.split(/<\/p>|<\/div>|\n/).filter((line: string) => line.trim());
|
||||
if (lines.length > 0) {
|
||||
let firstLine = lines[0];
|
||||
firstLine = firstLine.replace(/^<[^>]+>/, '');
|
||||
firstLine = stripHtmlTags(firstLine).trim();
|
||||
return firstLine || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
// 将祝福语内容按行分割(从第二行开始),去掉HTML标签
|
||||
const contentLines = computed(() => {
|
||||
if (!cardData.value?.zfContent) return [''];
|
||||
const content = cardData.value.zfContent;
|
||||
let lines = content.split(/<\/p>|<\/div>|\n/).filter((line: string) => line.trim());
|
||||
|
||||
// 去掉第一行(已用作称呼)
|
||||
if (lines.length > 0) {
|
||||
lines = lines.slice(1);
|
||||
}
|
||||
|
||||
// 去掉每行的HTML标签
|
||||
lines = lines.map((line: string) => {
|
||||
line = line.replace(/^<[^>]+>/, '');
|
||||
return stripHtmlTags(line).trim();
|
||||
}).filter((line: string) => line);
|
||||
|
||||
// 确保至少有5行
|
||||
const minLines = 5;
|
||||
while (lines.length < minLines) {
|
||||
lines.push('');
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
|
||||
// 加载贺卡详情
|
||||
const loadCardDetail = async () => {
|
||||
if (!recordId.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await srGetCardDetailByRecordApi({
|
||||
recordId: recordId.value,
|
||||
receiverId: receiverId.value || ''
|
||||
});
|
||||
|
||||
// 后端返回的数据结构:{resultCode: 1, message: "查询成功", result: {...}}
|
||||
// 兼容多种返回格式
|
||||
const cardDetail = res?.result || res?.data;
|
||||
const successCode = res?.resultCode === 1 || res?.code === 1 || res?.code === '1';
|
||||
|
||||
if (res && successCode && cardDetail) {
|
||||
cardData.value = cardDetail;
|
||||
|
||||
// 标记已查阅
|
||||
if (cardDetail.xxtsId) {
|
||||
await markAsViewed(cardDetail.xxtsId);
|
||||
}
|
||||
|
||||
// 初始化并播放背景音乐
|
||||
initAudio();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res?.message || res?.msg || '获取贺卡失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载贺卡详情失败:", e);
|
||||
uni.showToast({ title: "加载失败", icon: "none" });
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 标记已查阅
|
||||
const markAsViewed = async (xxtsId: string) => {
|
||||
if (!xxtsId) return;
|
||||
try {
|
||||
await srMarkAsViewedApi({ xxtsId });
|
||||
} catch (e) {
|
||||
console.error("标记已查阅失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化音频
|
||||
const initAudio = () => {
|
||||
const musicUrl = cardData.value?.hkMb?.musicUrl
|
||||
? imagUrl(cardData.value.hkMb.musicUrl)
|
||||
: '/static/base/music/srkl.mp3';
|
||||
|
||||
try {
|
||||
// #ifdef H5
|
||||
audioContext.value = new Audio(musicUrl);
|
||||
audioContext.value.loop = true;
|
||||
audioContext.value.play().catch((e: any) => {
|
||||
console.error("自动播放失败,可能需要用户交互:", e);
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
audioContext.value = uni.createInnerAudioContext();
|
||||
audioContext.value.src = musicUrl;
|
||||
audioContext.value.loop = true;
|
||||
audioContext.value.play();
|
||||
// #endif
|
||||
|
||||
isMusicPlaying.value = true;
|
||||
} catch (e) {
|
||||
console.error("初始化音频失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换音乐播放
|
||||
const toggleMusic = () => {
|
||||
if (!audioContext.value) return;
|
||||
|
||||
try {
|
||||
if (isMusicPlaying.value) {
|
||||
audioContext.value.pause();
|
||||
} else {
|
||||
audioContext.value.play();
|
||||
}
|
||||
isMusicPlaying.value = !isMusicPlaying.value;
|
||||
} catch (e) {
|
||||
console.error("音乐播放控制失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (audioContext.value) {
|
||||
try {
|
||||
audioContext.value.pause();
|
||||
// #ifdef MP-WEIXIN
|
||||
audioContext.value.destroy?.();
|
||||
// #endif
|
||||
} catch (e) {
|
||||
console.error("清理音频失败:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(async (options) => {
|
||||
recordId.value = options?.recordId || '';
|
||||
receiverId.value = options?.receiverId || '';
|
||||
|
||||
if (recordId.value) {
|
||||
await loadCardDetail();
|
||||
uni.setNavigationBarTitle({ title: "生日祝福" });
|
||||
} else {
|
||||
uni.showToast({ title: "缺少记录ID", icon: "none" });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-view-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
// 默认浅蓝色背景
|
||||
&.bg-blue {
|
||||
background: linear-gradient(180deg, #e6f3ff 0%, #cce5ff 50%, #b3d9ff 100%);
|
||||
|
||||
.letter-paper {
|
||||
background: linear-gradient(180deg, #f5faff 0%, #eef6ff 100%);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.header-line {
|
||||
background: linear-gradient(90deg, transparent 0%, #3b82f6 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.paper-footer {
|
||||
border-top-color: #93c5fd;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
background-color: rgba(230, 243, 255, 0.95);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border-color: #93c5fd;
|
||||
border-top-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
// 浅黄色背景
|
||||
&.bg-yellow {
|
||||
background: linear-gradient(180deg, #fdf6e3 0%, #f5e6c8 50%, #efe0b9 100%);
|
||||
|
||||
.letter-paper {
|
||||
background: linear-gradient(180deg, #fffef5 0%, #fff9e6 100%);
|
||||
border-color: rgba(201, 162, 39, 0.2);
|
||||
}
|
||||
|
||||
.header-line {
|
||||
background: linear-gradient(90deg, transparent 0%, #c9a227 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
.paper-footer {
|
||||
border-top-color: #e8d5a3;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
background-color: rgba(253, 246, 227, 0.95);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border-color: #e8d5a3;
|
||||
border-top-color: #c9a227;
|
||||
}
|
||||
}
|
||||
|
||||
// 浅紫色背景
|
||||
&.bg-purple {
|
||||
background: linear-gradient(180deg, #f3e8ff 0%, #e9d5ff 50%, #ddd6fe 100%);
|
||||
|
||||
.letter-paper {
|
||||
background: linear-gradient(180deg, #faf5ff 0%, #f5f0ff 100%);
|
||||
border-color: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.header-line {
|
||||
background: linear-gradient(90deg, transparent 0%, #8b5cf6 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: #6b21a8;
|
||||
}
|
||||
|
||||
.paper-footer {
|
||||
border-top-color: #c4b5fd;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
background-color: rgba(243, 232, 255, 0.95);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border-color: #c4b5fd;
|
||||
border-top-color: #8b5cf6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载遮罩 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 贺卡容器 */
|
||||
.card-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
padding-top: calc(20px + env(safe-area-inset-top));
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 信纸主体 */
|
||||
.letter-paper {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
padding: 30px 25px;
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.1),
|
||||
0 1px 3px rgba(0, 0, 0, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 信纸左边红色竖线 */
|
||||
.letter-paper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 80px;
|
||||
bottom: 100px;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, #e74c3c 0%, #c0392b 100%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 信纸顶部装饰 */
|
||||
.paper-header {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.header-line {
|
||||
height: 2px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "KaiTi", "STKaiti", "楷体", serif;
|
||||
}
|
||||
|
||||
.header-date {
|
||||
font-size: 14px;
|
||||
color: #a08060;
|
||||
}
|
||||
|
||||
/* 称呼 */
|
||||
.letter-greeting {
|
||||
margin-bottom: 20px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.greeting-line {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #d4c4a8;
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
font-size: 18px;
|
||||
color: #5a4a3a;
|
||||
font-family: "KaiTi", "STKaiti", "楷体", serif;
|
||||
}
|
||||
|
||||
/* 信件正文 */
|
||||
.letter-content {
|
||||
flex: 1;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content-line {
|
||||
position: relative;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.line-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: #4a3a2a;
|
||||
font-family: "KaiTi", "STKaiti", "楷体", serif;
|
||||
text-indent: 2em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.line-border {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
border-bottom: 1px dashed #d4c4a8;
|
||||
}
|
||||
|
||||
/* 签名区域 */
|
||||
.letter-signature {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.signature-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.signature-info {
|
||||
text-align: right;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.signer-label {
|
||||
font-size: 14px;
|
||||
color: #8b7355;
|
||||
display: inline;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.signer-name {
|
||||
font-size: 18px;
|
||||
color: #5a4a3a;
|
||||
font-weight: 500;
|
||||
font-family: "KaiTi", "STKaiti", "楷体", serif;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.signature-date {
|
||||
font-size: 14px;
|
||||
color: #a08060;
|
||||
border-bottom: 1px dashed #d4c4a8;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 信纸底部装饰 */
|
||||
.paper-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.footer-decoration {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
font-size: 24px;
|
||||
animation: float-icon 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.footer-icon:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.footer-icon:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes float-icon {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
/* 右上角音乐图标 */
|
||||
.music-icon-wrapper {
|
||||
position: fixed;
|
||||
top: calc(20px + env(safe-area-inset-top));
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.music-icon-wrapper:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.music-icon-wrapper.playing .music-icon-emoji {
|
||||
animation: music-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.music-icon-emoji {
|
||||
font-size: 28px;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes music-rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -250,7 +250,7 @@ import { attachmentUpload } from "@/api/system/upload";
|
||||
import BasicLayout from "@/components/BasicLayout/Layout.vue";
|
||||
import { ImageVideoUpload } from "@/components/ImageVideoUpload";
|
||||
import BasicTree from '@/components/BasicTree/Tree.vue';
|
||||
import { findAllNjBjTree } from '@/api/base/server';
|
||||
import { findAllNjBjKzTreeApi } from '@/api/base/server';
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
@ -325,7 +325,7 @@ const formatTime = (timestamp: string) => {
|
||||
// 加载树形数据
|
||||
const loadTreeData = async () => {
|
||||
try {
|
||||
const res = await findAllNjBjTree();
|
||||
const res = await findAllNjBjKzTreeApi();
|
||||
if (res.resultCode === 1 && res.result) {
|
||||
// 递归转换数据格式以适配 BasicTree 组件
|
||||
const convertTreeData = (items: any[]): any[] => {
|
||||
|
||||
BIN
src/static/base/home/srqd.png
Normal file
BIN
src/static/base/home/srqd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/static/base/music/srkl.mp3
Normal file
BIN
src/static/base/music/srkl.mp3
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user