库存
This commit is contained in:
parent
52db1848a8
commit
6cf8f9cdd9
@ -55,6 +55,7 @@
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.6",
|
||||
"jest": "27.0.4",
|
||||
"jweixin-js-sdk": "^1.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"pinia": "2.0.23",
|
||||
"pinia-plugin-persist-uni": "1.2.0",
|
||||
|
||||
138
src/api/base/invApplyApi.ts
Normal file
138
src/api/base/invApplyApi.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 分页查询领用申请
|
||||
*/
|
||||
export function invApplyFindPageApi(params: any) {
|
||||
return get('/api/invApply/findPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询领用申请
|
||||
*/
|
||||
export function invApplyFindByIdApi(params: any) {
|
||||
return get('/api/invApply/findById', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询领用申请(完整信息)
|
||||
*/
|
||||
export function invApplyFindFullByIdApi(params: any) {
|
||||
return get('/api/invApply/findByIdFull', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存领用申请(新增/修改)
|
||||
*/
|
||||
export function invApplySaveApi(params: any) {
|
||||
return post('/api/invApply/save', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除领用申请
|
||||
*/
|
||||
export function invApplyDeleteApi(params: any) {
|
||||
return post('/api/invApply/delete', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交领用申请(走审批流程)
|
||||
*/
|
||||
export function invApplySqApi(params: any) {
|
||||
return post('/api/invApply/sq', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新提交领用申请
|
||||
*/
|
||||
export function invApplyReSubmitApi(params: any) {
|
||||
return post('/api/invApply/reSubmit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回领用申请
|
||||
*/
|
||||
export function invApplyWithdrawApi(params: { id: string; spRemark?: string }) {
|
||||
let url = `/api/invApply/withdraw?id=${params.id}`;
|
||||
if (params.spRemark) {
|
||||
url += `&spRemark=${encodeURIComponent(params.spRemark)}`;
|
||||
}
|
||||
return post(url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询领用申请流程详情
|
||||
*/
|
||||
export function invApplyFlowByIdApi(params: any) {
|
||||
return get('/api/invApply/flowById', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据申请单ID查询明细列表
|
||||
*/
|
||||
export function invApplyItemFindPageApi(params: any) {
|
||||
return get('/api/invApplyItem/findPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据申请单ID查询明细列表(按申请单ID)
|
||||
*/
|
||||
export function invApplyItemFindByApplyIdApi(applyId: string) {
|
||||
return get('/api/invApplyItem/findByApplyId', { applyId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存明细
|
||||
*/
|
||||
export function invApplyItemSaveBatchApi(params: any) {
|
||||
return post('/api/invApplyItem/saveBatch', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仓库列表
|
||||
*/
|
||||
export function invWarehouseFindAllApi(params: any) {
|
||||
return get('/api/invWarehouse/findAll', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询物品列表
|
||||
*/
|
||||
export function invItemFindAllApi(params: any) {
|
||||
return get('/api/invItem/findAll', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询库位列表
|
||||
*/
|
||||
export function invLocationFindAllApi(params: any) {
|
||||
return get('/api/invLocation/findAll', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批领用申请
|
||||
*/
|
||||
export function invApplySpApi(params: any) {
|
||||
return post('/api/invApply/sp', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办领用申请
|
||||
*/
|
||||
export function invApplyTransferApi(params: any) {
|
||||
return post('/api/invApply/transfer', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止领用申请
|
||||
*/
|
||||
export function invApplyStopApi(params: any) {
|
||||
return post('/api/invApply/stop', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 领用申请待办/已办列表
|
||||
*/
|
||||
export function invApplyFindUserTodosPageApi(params: any) {
|
||||
return get('/api/invApply/findUserTodosPage', params);
|
||||
}
|
||||
23
src/api/base/invItemApi.ts
Normal file
23
src/api/base/invItemApi.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 查询物品列表(全部)
|
||||
*/
|
||||
export function invItemFindAllApi(params?: any) {
|
||||
return get('/api/invItem/findAll', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询物品列表
|
||||
*/
|
||||
export function invItemFindPageApi(params: any) {
|
||||
return get('/api/invItem/findPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询物品
|
||||
*/
|
||||
export function invItemFindByIdApi(params: any) {
|
||||
return get('/api/invItem/findById', params);
|
||||
}
|
||||
|
||||
37
src/api/base/invOutBillApi.ts
Normal file
37
src/api/base/invOutBillApi.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 分页查询出库单
|
||||
*/
|
||||
export function invOutBillFindPageApi(params: any) {
|
||||
return get('/api/invOutBill/findPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询出库单
|
||||
*/
|
||||
export function invOutBillFindByIdApi(params: any) {
|
||||
return get('/api/invOutBill/findById', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存出库单(新增/修改)
|
||||
*/
|
||||
export function invOutBillSaveApi(params: any) {
|
||||
return post('/api/invOutBill/save', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除出库单
|
||||
*/
|
||||
export function invOutBillDeleteApi(params: any) {
|
||||
return post('/api/invOutBill/delete', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部出库单
|
||||
*/
|
||||
export function invOutBillFindAllApi() {
|
||||
return get('/api/invOutBill/findAll', {});
|
||||
}
|
||||
|
||||
@ -93,6 +93,18 @@ export function jfReSubmitApi(params: any) {
|
||||
return post('/api/jf/reSubmit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分审批-撤回
|
||||
*/
|
||||
export function jfWithdrawApi(params: { id: string; spRemark?: string }) {
|
||||
// 将参数作为查询字符串传递
|
||||
let url = `/api/jf/withdraw?id=${params.id}`;
|
||||
if (params.spRemark) {
|
||||
url += `&spRemark=${encodeURIComponent(params.spRemark)}`;
|
||||
}
|
||||
return post(url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分审批-流程详情
|
||||
*/
|
||||
@ -135,3 +147,34 @@ export function jfStudentAwardDeleteApi(params: any) {
|
||||
return post('/api/jfXsqd/delete', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出教师积分明细
|
||||
*/
|
||||
export function jfExportApi(params: any) {
|
||||
return get('/api/jf/export', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按申请人分组统计待审批记录
|
||||
* @param params { jsId: 审批人ID, dbZt: 待办状态(A/B), spType: 审批类型(SP/CC), page: 页码, rows: 每页条数 }
|
||||
*/
|
||||
export function jfFindUserTodosGroupByApplicantApi(params: any) {
|
||||
return get('/api/jf/findUserTodosGroupByApplicant', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定申请人的所有待审批记录详情
|
||||
* @param params { applicantId: 申请人ID, jsId: 审批人ID, dbZt: 待办状态, spType: 审批类型 }
|
||||
*/
|
||||
export function jfFindApplicantTodosDetailApi(params: any) {
|
||||
return get('/api/jf/findApplicantTodosDetail', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量审批接口
|
||||
* @param params { records: [{ xxtsId, ywId, spStatus, spRemark }], spStatus: 审批状态, spRemark: 审批意见 }
|
||||
*/
|
||||
export function jfBatchSpApi(params: any) {
|
||||
return post('/api/jf/batchSp', params);
|
||||
}
|
||||
|
||||
|
||||
@ -189,6 +189,11 @@ export const xsFindList = async (params: any) => {
|
||||
return await get("/api/xs/findPage", params);
|
||||
};
|
||||
|
||||
// 获取学生列表(不受权限控制)
|
||||
export const xsFindListKz = async (params: any) => {
|
||||
return await get("/api/xs/findPageKz", params);
|
||||
};
|
||||
|
||||
// 获取学生家长列表
|
||||
export const xsJzListByXsIdApi = async (params: any) => {
|
||||
return await get("/api/jz/getListByXsId", params);
|
||||
|
||||
@ -136,6 +136,13 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/jc/jcList",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就餐统计",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/jc/grade-statistics",
|
||||
"style": {
|
||||
@ -876,6 +883,20 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfsp/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "审批列表",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfsp/compare",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分审批",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/jfdj/index",
|
||||
"style": {
|
||||
@ -911,6 +932,48 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invsq/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "领用申请",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invsq/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "领用详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invsq/Apply",
|
||||
"style": {
|
||||
"navigationBarTitleText": "领用申请",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invsp/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "领用审批",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invsp/InvApplyApprove",
|
||||
"style": {
|
||||
"navigationBarTitleText": "领用审批详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/inv/invout/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "物品出库",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/wj/index",
|
||||
"style": {
|
||||
|
||||
@ -710,6 +710,40 @@ const sections = reactive<Section[]>([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "routine-inv",
|
||||
icon: "inv",
|
||||
text: "库存管理",
|
||||
show: true,
|
||||
permissionKey: "routine-inv",
|
||||
isFolder: true,
|
||||
folderItems: [
|
||||
{
|
||||
id: "routine-inv-ly",
|
||||
icon: "invly",
|
||||
text: "领用申请",
|
||||
show: true,
|
||||
permissionKey: "routine-invly",
|
||||
path: "/pages/view/routine/inv/invsq/index",
|
||||
},
|
||||
{
|
||||
id: "routine-inv-sp",
|
||||
icon: "invsp",
|
||||
text: "领用审批",
|
||||
show: true,
|
||||
permissionKey: "routine-invsp",
|
||||
path: "/pages/view/routine/inv/invsp/index",
|
||||
},
|
||||
{
|
||||
id: "routine-inv-out",
|
||||
icon: "invout",
|
||||
text: "物品出库",
|
||||
show: true,
|
||||
permissionKey: "routine-invout",
|
||||
path: "/pages/view/routine/inv/invout/index",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -845,6 +879,14 @@ const sections = reactive<Section[]>([
|
||||
permissionKey: "routine-jcdm", // 就餐点名权限编码
|
||||
path: "/pages/view/routine/jc/bzList",
|
||||
},
|
||||
{
|
||||
id: "r11",
|
||||
icon: "jctjqd",
|
||||
text: "就餐统计",
|
||||
show: true,
|
||||
permissionKey: "routine-jctjqd", // 就餐统计权限编码
|
||||
path: "/pages/statistics/jc/jcList",
|
||||
},
|
||||
{
|
||||
id: "r9",
|
||||
icon: "jlfb",
|
||||
|
||||
543
src/pages/components/InvItemSelector/index.vue
Normal file
543
src/pages/components/InvItemSelector/index.vue
Normal file
@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<view class="item-selector-modal" v-if="visible" @click="handleMaskClick">
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-selector-content" @click.stop>
|
||||
<!-- 头部 -->
|
||||
<view class="selector-header">
|
||||
<view class="header-title">选择物品</view>
|
||||
<view class="header-actions">
|
||||
<button class="close-btn" @click="closeModal">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<view class="search-input-container">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入物品名称"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<button class="search-btn" @click="handleSearch">搜索</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<scroll-view
|
||||
class="table-container"
|
||||
scroll-y="true"
|
||||
:style="{ height: listHeight + 'px' }"
|
||||
@scrolltolower="loadMore"
|
||||
enable-back-to-top="true"
|
||||
>
|
||||
<view class="table-wrapper">
|
||||
<!-- 表头 -->
|
||||
<view class="table-header">
|
||||
<view class="table-col col-radio"></view>
|
||||
<view class="table-col col-name">物品名称</view>
|
||||
<view class="table-col col-spec">规格</view>
|
||||
<view class="table-col col-stock">库存</view>
|
||||
<view class="table-col col-unit">单位</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<view class="table-body">
|
||||
<view
|
||||
class="table-row"
|
||||
v-for="item in allItems"
|
||||
:key="item.id"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<view class="table-col col-radio">
|
||||
<radio
|
||||
:checked="selectedItemId === item.id"
|
||||
@click.stop="selectItem(item)"
|
||||
/>
|
||||
</view>
|
||||
<view class="table-col col-name">{{ item.itemName || '-' }}</view>
|
||||
<view class="table-col col-spec">{{ item.specModel || '-' }}</view>
|
||||
<view class="table-col col-stock">{{ item.availableQty !== undefined && item.availableQty !== null ? item.availableQty : (item.qty !== undefined && item.qty !== null ? item.qty : '-') }}</view>
|
||||
<view class="table-col col-unit">{{ item.unitName || '-' }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="allItems.length === 0 && !loading" class="empty-tips">
|
||||
暂无物品数据
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="loadingMore" class="loading-more">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="hasMore === false && allItems.length > 0" class="no-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<view class="footer-actions">
|
||||
<button class="confirm-btn" @click="handleConfirm" :disabled="!selectedItemId">
|
||||
确认
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { invItemFindAllApi, invItemFindPageApi } from '@/api/base/invItemApi';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
modelValue?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
modelValue: false
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
'update:modelValue': [value: boolean];
|
||||
'confirm': [item: any];
|
||||
'cancel': [];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const searchKeyword = ref('');
|
||||
const allItems = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const listHeight = ref(400);
|
||||
const selectedItemId = ref<string | null>(null);
|
||||
const selectedItemData = ref<any>(null);
|
||||
|
||||
// 计算列表高度
|
||||
const calculateListHeight = () => {
|
||||
// 在 H5 环境中使用 window 对象,在小程序中使用 uni
|
||||
let windowHeight;
|
||||
if (typeof window !== 'undefined') {
|
||||
// H5 环境
|
||||
windowHeight = window.innerHeight;
|
||||
} else {
|
||||
// 小程序环境
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
windowHeight = systemInfo.windowHeight;
|
||||
}
|
||||
// 减去头部、搜索和分类区域的高度
|
||||
listHeight.value = windowHeight - 200;
|
||||
};
|
||||
|
||||
// 加载物品数据
|
||||
const loadItems = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const params: any = {
|
||||
page: currentPage.value,
|
||||
rows: pageSize.value
|
||||
};
|
||||
|
||||
// 只按物品名称搜索
|
||||
if (searchKeyword.value && searchKeyword.value.trim()) {
|
||||
params.itemName = searchKeyword.value.trim();
|
||||
}
|
||||
|
||||
const res: any = await invItemFindPageApi(params);
|
||||
// 处理返回格式:{total: 1, page: 1, records: 2, rows: [...]}
|
||||
let items: any[] = [];
|
||||
if (res?.rows && Array.isArray(res.rows)) {
|
||||
items = res.rows;
|
||||
} else if (res?.result?.rows && Array.isArray(res.result.rows)) {
|
||||
items = res.result.rows;
|
||||
} else if (Array.isArray(res)) {
|
||||
items = res;
|
||||
}
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
allItems.value = items;
|
||||
} else {
|
||||
allItems.value = [...allItems.value, ...items];
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
const total = res?.total || res?.records || 0;
|
||||
hasMore.value = allItems.value.length < total && items.length === pageSize.value;
|
||||
} catch (error) {
|
||||
console.error('加载物品数据失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = async () => {
|
||||
currentPage.value = 1;
|
||||
await loadItems();
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
emit('update:visible', false);
|
||||
emit('update:modelValue', false);
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 点击遮罩层关闭
|
||||
const handleMaskClick = () => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// 切换选择
|
||||
const toggleSelect = (item: any) => {
|
||||
if (selectedItemId.value === item.id) {
|
||||
selectedItemId.value = null;
|
||||
selectedItemData.value = null;
|
||||
} else {
|
||||
selectedItemId.value = item.id;
|
||||
selectedItemData.value = item;
|
||||
}
|
||||
};
|
||||
|
||||
// 选择物品(单选框点击)
|
||||
const selectItem = (item: any) => {
|
||||
selectedItemId.value = item.id;
|
||||
selectedItemData.value = item;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedItemData.value) {
|
||||
// 传递完整的物品数据,包括可用库存数量
|
||||
const itemData = {
|
||||
...selectedItemData.value,
|
||||
// 优先使用可用库存,如果没有则使用总库存
|
||||
qty: selectedItemData.value.availableQty !== undefined && selectedItemData.value.availableQty !== null
|
||||
? selectedItemData.value.availableQty
|
||||
: (selectedItemData.value.qty !== undefined && selectedItemData.value.qty !== null
|
||||
? selectedItemData.value.qty
|
||||
: 0),
|
||||
// 同时传递总库存和可用库存
|
||||
totalQty: selectedItemData.value.qty !== undefined && selectedItemData.value.qty !== null
|
||||
? selectedItemData.value.qty
|
||||
: 0,
|
||||
availableQty: selectedItemData.value.availableQty !== undefined && selectedItemData.value.availableQty !== null
|
||||
? selectedItemData.value.availableQty
|
||||
: (selectedItemData.value.qty !== undefined && selectedItemData.value.qty !== null
|
||||
? selectedItemData.value.qty
|
||||
: 0)
|
||||
};
|
||||
emit('confirm', itemData);
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!hasMore.value || loadingMore.value) return;
|
||||
|
||||
loadingMore.value = true;
|
||||
currentPage.value++;
|
||||
await loadItems();
|
||||
};
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
searchKeyword.value = '';
|
||||
allItems.value = [];
|
||||
currentPage.value = 1;
|
||||
hasMore.value = true;
|
||||
selectedItemId.value = null;
|
||||
selectedItemData.value = null;
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
// 计算列表高度
|
||||
calculateListHeight();
|
||||
// 加载物品数据(不传搜索关键词,加载全部)
|
||||
searchKeyword.value = '';
|
||||
currentPage.value = 1;
|
||||
loadItems();
|
||||
} else {
|
||||
// 关闭时重置数据
|
||||
resetData();
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-selector-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1001;
|
||||
|
||||
.loading-content {
|
||||
min-width: 160rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-selector-content {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.selector-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.close-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
|
||||
.search-input-container {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
height: 60rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
|
||||
.table-wrapper {
|
||||
.table-header {
|
||||
display: flex;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 2rpx solid #e4e7ed;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
.table-col {
|
||||
padding: 20rpx 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #e4e7ed;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.col-radio {
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.col-name {
|
||||
flex: 2;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
&.col-spec {
|
||||
flex: 1.5;
|
||||
min-width: 150rpx;
|
||||
}
|
||||
|
||||
&.col-stock {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
&.col-unit {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-body {
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
background: #fff;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.table-col {
|
||||
padding: 24rpx 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.col-radio {
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.col-name {
|
||||
flex: 2;
|
||||
min-width: 200rpx;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.col-spec {
|
||||
flex: 1.5;
|
||||
min-width: 150rpx;
|
||||
}
|
||||
|
||||
&.col-stock {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
&.col-unit {
|
||||
flex: 1;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tips {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
padding: 24rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
background: #fff;
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&:disabled {
|
||||
background: #c0c4cc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
253
src/pages/components/PdfPreview/index.vue
Normal file
253
src/pages/components/PdfPreview/index.vue
Normal file
@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<view class="pdf-preview-container" v-if="visible">
|
||||
<!-- PDF 内容区域(全屏显示) -->
|
||||
<view class="pdf-content" v-if="!loading && !error && pdfViewerUrl">
|
||||
<!-- 使用 web-view 加载 PDF.js 预览页 -->
|
||||
<web-view :src="pdfViewerUrl" @load="handleWebViewLoad" @error="handleWebViewError"></web-view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中(全屏) -->
|
||||
<view v-if="loading" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">正在加载PDF...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示(全屏) -->
|
||||
<view v-if="error" class="error-container">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-text">{{ errorMessage }}</text>
|
||||
<view class="retry-btn" @click="retryLoad">
|
||||
<text class="retry-text">重试</text>
|
||||
</view>
|
||||
<view class="close-btn" @click="handleClose">
|
||||
<text class="close-text">关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onBeforeUnmount } from 'vue';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
pdfUrl: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
pdfUrl: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const pdfViewerUrl = ref('');
|
||||
const pdfBlobUrl = ref(''); // 存储下载后的 blob URL
|
||||
|
||||
// 清理资源
|
||||
const cleanup = () => {
|
||||
// 清理 blob URL,避免内存泄漏
|
||||
if (pdfBlobUrl.value) {
|
||||
URL.revokeObjectURL(pdfBlobUrl.value);
|
||||
pdfBlobUrl.value = '';
|
||||
console.log('Blob URL已释放');
|
||||
}
|
||||
pdfViewerUrl.value = '';
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
errorMessage.value = '';
|
||||
};
|
||||
|
||||
// 加载PDF
|
||||
const loadPdf = (url: string) => {
|
||||
if (!url) {
|
||||
error.value = true;
|
||||
errorMessage.value = '缺少PDF URL参数';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
errorMessage.value = '';
|
||||
|
||||
// 设置PDF查看器URL
|
||||
pdfViewerUrl.value = url;
|
||||
|
||||
// 如果是blob URL,保存以便清理
|
||||
if (url.startsWith('blob:')) {
|
||||
pdfBlobUrl.value = url;
|
||||
}
|
||||
|
||||
// 延迟关闭loading,确保web-view已加载
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 监听 visible 和 pdfUrl 变化,使用 watchEffect 确保两者都准备好
|
||||
watch([() => props.visible, () => props.pdfUrl], ([newVisible, newPdfUrl]) => {
|
||||
console.log('[PdfPreview] visible 或 pdfUrl 变化:', { visible: newVisible, pdfUrl: newPdfUrl });
|
||||
|
||||
if (newVisible && newPdfUrl) {
|
||||
// 两者都准备好时,加载PDF
|
||||
console.log('[PdfPreview] 开始加载PDF:', newPdfUrl);
|
||||
loadPdf(newPdfUrl);
|
||||
} else if (!newVisible) {
|
||||
// 关闭时清理资源
|
||||
console.log('[PdfPreview] 关闭预览,清理资源');
|
||||
cleanup();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 组件销毁前清理资源
|
||||
onBeforeUnmount(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// web-view 加载完成
|
||||
const handleWebViewLoad = (e: any) => {
|
||||
console.log('Web-view加载完成(PDF.js查看器已就绪):', e);
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// web-view 加载失败
|
||||
const handleWebViewError = (e: any) => {
|
||||
console.error('Web-view加载失败:', e);
|
||||
loading.value = false;
|
||||
error.value = true;
|
||||
errorMessage.value = 'PDF加载失败,请检查网络连接或重试';
|
||||
};
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = () => {
|
||||
if (props.pdfUrl) {
|
||||
loadPdf(props.pdfUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭预览
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pdf-preview-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #525659;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
// PDF 内容(全屏显示)
|
||||
.pdf-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
// 加载中(全屏显示)
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #525659;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 错误提示(全屏显示)
|
||||
.error-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #525659;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retry-btn,
|
||||
.close-btn {
|
||||
padding: 10px 30px;
|
||||
background: #667eea;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.retry-text,
|
||||
.close-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
430
src/pages/statistics/jc/jcList.vue
Normal file
430
src/pages/statistics/jc/jcList.vue
Normal file
@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<view class="jc-list-container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">就餐标准列表</text>
|
||||
</view>
|
||||
|
||||
<!-- 就餐列表 -->
|
||||
<view class="section" v-if="jcBzList.length > 0">
|
||||
<!-- 就餐列表 -->
|
||||
<view class="jc-list">
|
||||
<view
|
||||
v-for="bz in jcBzList"
|
||||
:key="bz.id"
|
||||
class="jc-item bg-white r-md p-12"
|
||||
@click="goToBz(bz)"
|
||||
>
|
||||
<view class="jc-header">
|
||||
<view class="jc-title">{{ bz.bzMc }}</view>
|
||||
</view>
|
||||
|
||||
<view class="jc-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">学期名称:</text>
|
||||
<text class="info-value">{{ bz.xqMc || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">年级范围:</text>
|
||||
<text class="info-value">{{ bz.njmc || '全部' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">说明:</text>
|
||||
<text class="info-value">{{ bz.bzSm }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && jcBzList.length === 0 && hasSearched">
|
||||
<view class="empty-icon">📚</view>
|
||||
<view class="empty-text">暂无就餐数据</view>
|
||||
<view class="empty-tip">请联系管理员添加就餐信息</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<view class="loading-text">正在加载...</view>
|
||||
</view>
|
||||
|
||||
<!-- 滚动加载更多 -->
|
||||
<view class="load-more" v-if="hasMore && !loading">
|
||||
<view class="load-more-text">上拉加载更多</view>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view class="no-more" v-if="!hasMore && hasSearched && jcBzList.length > 0">
|
||||
<view class="no-more-text">没有更多数据了</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { jcBzFindPageApi } from '@/api/base/jcApi'
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
import dayjs from 'dayjs';
|
||||
const { setJcBz } = useDataStore();
|
||||
|
||||
// 响应式数据
|
||||
const jcBzList = ref<any>([])
|
||||
const loading = ref(false)
|
||||
const hasSearched = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
console.log('就餐列表页面加载')
|
||||
loadJcBzList()
|
||||
})
|
||||
|
||||
// 加载就餐列表
|
||||
const loadJcBzList = async (isLoadMore = false) => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
hasSearched.value = true
|
||||
|
||||
try {
|
||||
// 调用后端接口查询就餐列表
|
||||
const response = await jcBzFindPageApi({
|
||||
pageNo: currentPage.value,
|
||||
rows: 10,
|
||||
sfFb: 1, // 查询已发布的数据
|
||||
});
|
||||
|
||||
console.log('API返回结果:', response)
|
||||
|
||||
if (response && response.rows) {
|
||||
const newData = response.rows || []
|
||||
|
||||
if (isLoadMore) {
|
||||
// 加载更多时追加数据
|
||||
jcBzList.value = [...jcBzList.value, ...newData]
|
||||
} else {
|
||||
// 首次加载或刷新时替换数据
|
||||
jcBzList.value = newData
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore.value = newData.length >= pageSize.value
|
||||
} else {
|
||||
throw new Error('查询失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询就餐列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '查询失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
if (!isLoadMore) {
|
||||
jcBzList.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多数据
|
||||
const loadMore = () => {
|
||||
if (loading.value || !hasMore.value) {
|
||||
return
|
||||
}
|
||||
|
||||
currentPage.value++
|
||||
loadJcBzList(true)
|
||||
}
|
||||
|
||||
// 滚动到底部触发加载更多
|
||||
const onReachBottom = () => {
|
||||
loadMore()
|
||||
}
|
||||
|
||||
// 跳转到报名统计年级页面
|
||||
const goToBz = (bz: any) => {
|
||||
setJcBz(bz);
|
||||
|
||||
// 解析 jfJsSj 字段,提取月份(YYYY-MM格式)
|
||||
let month = dayjs().format('YYYY-MM'); // 默认使用当前月份
|
||||
|
||||
if (bz.jfJsSj) {
|
||||
try {
|
||||
// jfJsSj 格式: "2025-09-27 09:57:56"
|
||||
// 提取前10个字符(YYYY-MM-DD),然后提取前7个字符(YYYY-MM)
|
||||
const dateStr = bz.jfJsSj.trim();
|
||||
if (dateStr.length >= 7) {
|
||||
month = dateStr.substring(0, 7); // 提取 "YYYY-MM"
|
||||
console.log('从 jfJsSj 解析月份:', bz.jfJsSj, '->', month);
|
||||
} else {
|
||||
console.warn('jfJsSj 格式不正确,使用当前月份:', bz.jfJsSj);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析 jfJsSj 失败,使用当前月份:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('jfJsSj 字段不存在,使用当前月份');
|
||||
}
|
||||
|
||||
// 默认统计类型为未报名统计
|
||||
const defaultType = 'unregistered';
|
||||
|
||||
// 跳转到报名统计年级页面,传递月份和统计类型参数
|
||||
uni.navigateTo({
|
||||
url: `/pages/statistics/jc/registration-grade-statistics?type=${defaultType}&month=${month}`
|
||||
});
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case '未开始':
|
||||
return 'status-pending'
|
||||
case '进行中':
|
||||
return 'status-active'
|
||||
case '已结束':
|
||||
return 'status-ended'
|
||||
default:
|
||||
return 'status-unknown'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jc-list-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.jc-list {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.jc-item {
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.jc-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 15rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.jc-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.jc-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.jc-status {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.status-pending {
|
||||
background-color: #fff7e6;
|
||||
color: #fa8c16;
|
||||
border: 1rpx solid #ffd591;
|
||||
}
|
||||
|
||||
&.status-active {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1rpx solid #b7eb8f;
|
||||
}
|
||||
|
||||
&.status-ended {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1rpx solid #ffccc7;
|
||||
}
|
||||
|
||||
&.status-unknown {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
border: 1rpx solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.jc-info {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 40rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 100rpx;
|
||||
|
||||
.loading-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.r-md {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.p-12 {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
/* 滚动加载样式 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.no-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
|
||||
.no-more-text {
|
||||
font-size: 28rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -6,6 +6,15 @@
|
||||
<text class="header-title">{{ pageTitle }}</text>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<!-- #ifdef H5 -->
|
||||
<u-icon
|
||||
v-if="isWeixin"
|
||||
name="share"
|
||||
size="20"
|
||||
@click="handleShareClick"
|
||||
style="margin-right: 20rpx;"
|
||||
></u-icon>
|
||||
<!-- #endif -->
|
||||
<u-button size="mini" @click="refreshPage">刷新</u-button>
|
||||
</view>
|
||||
</view>
|
||||
@ -20,11 +29,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { initWechatShare, isWeixinBrowser, buildShareLink } from '@/utils/wechatShare';
|
||||
import { imagUrl } from '@/utils';
|
||||
|
||||
const previewUrl = ref('');
|
||||
const pageTitle = ref('文件预览');
|
||||
const isWeixin = ref(false);
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.url) {
|
||||
@ -37,6 +49,76 @@ onLoad((options) => {
|
||||
console.log('Web-View预览URL:', previewUrl.value);
|
||||
});
|
||||
|
||||
// 初始化微信分享
|
||||
const initShare = async () => {
|
||||
// #ifdef H5
|
||||
// 检测是否在微信浏览器环境
|
||||
isWeixin.value = isWeixinBrowser();
|
||||
|
||||
if (!isWeixin.value) {
|
||||
console.log('当前不在微信浏览器环境,跳过分享初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建分享链接
|
||||
// 获取当前页面完整路径(包含hash路由)
|
||||
const currentPath = window.location.href;
|
||||
|
||||
// 构建分享链接,包含预览URL和标题参数
|
||||
const shareLink = buildShareLink(currentPath, {
|
||||
url: previewUrl.value,
|
||||
title: pageTitle.value
|
||||
});
|
||||
|
||||
// 获取封面图URL,添加时间戳避免缓存
|
||||
const logoUrl = imagUrl('/upload/zhxy/report/logo.jpg');
|
||||
const imgUrl = logoUrl ? `${logoUrl}${logoUrl.includes('?') ? '&' : '?'}t=${Date.now()}` : '';
|
||||
|
||||
// 初始化分享配置
|
||||
await initWechatShare({
|
||||
title: pageTitle.value || '文件预览',
|
||||
desc: `点击查看:${pageTitle.value}`,
|
||||
link: shareLink,
|
||||
imgUrl: imgUrl // 使用默认的logo作为封面图,添加时间戳避免缓存
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 分享按钮点击事件(可选,主要用于手动触发)
|
||||
const handleShareClick = () => {
|
||||
// #ifdef H5
|
||||
if (isWeixin.value) {
|
||||
// 在微信中,点击右上角分享按钮会自动触发分享
|
||||
// 这里可以显示提示信息
|
||||
uni.showToast({
|
||||
title: '请点击右上角分享按钮',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化分享
|
||||
onMounted(() => {
|
||||
// 延迟一下,确保微信JS-SDK已加载
|
||||
setTimeout(() => {
|
||||
initShare();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 页面显示时重新设置分享内容(解决分享缓存问题)
|
||||
onShow(() => {
|
||||
// #ifdef H5
|
||||
// 每次页面显示时重新设置分享内容,避免使用其他页面的分享数据
|
||||
if (isWeixin.value) {
|
||||
setTimeout(() => {
|
||||
initShare();
|
||||
}, 300);
|
||||
}
|
||||
// #endif
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
@ -958,9 +958,9 @@ const handleAddStudent = () => {
|
||||
// 注册监听
|
||||
uni.$once('studentPickerConfirm', handleStudentConfirm);
|
||||
|
||||
// 跳转到学生选择页面
|
||||
// 跳转到学生选择页面(使用不受权限控制的接口)
|
||||
uni.navigateTo({
|
||||
url: `/pages/components/StudentPicker/index?selectedIds=${selectedIds.join(',')}&multiple=true`
|
||||
url: `/pages/components/StudentPicker/index?selectedIds=${selectedIds.join(',')}&multiple=true&useKzTree=true`
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -29,6 +29,10 @@
|
||||
<uni-card :is-shadow="false" is-full>
|
||||
<view class="header">
|
||||
<text class="score">业绩积分: {{ totalScore }}</text>
|
||||
<view class="header-actions">
|
||||
<text class="detail-btn" @click="handleGoToDetail">明细</text>
|
||||
<text class="export-btn" @click="showExportDialog">导出</text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
@ -75,13 +79,61 @@
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 导出格式选择弹窗 -->
|
||||
<view class="export-modal" v-if="showExportModal" @click="closeExportDialog">
|
||||
<view class="export-modal-content" @click.stop>
|
||||
<view class="export-modal-header">
|
||||
<text class="export-modal-title">选择导出格式</text>
|
||||
<text class="export-modal-close" @click="closeExportDialog">×</text>
|
||||
</view>
|
||||
<view class="export-modal-body">
|
||||
<view
|
||||
class="export-option-item"
|
||||
:class="{ selected: selectedExportFormat === 'excel' }"
|
||||
@click="selectExportFormat('excel')"
|
||||
>
|
||||
<view class="option-radio">
|
||||
<view class="radio-dot" v-if="selectedExportFormat === 'excel'"></view>
|
||||
</view>
|
||||
<text class="option-text">Excel 表格</text>
|
||||
</view>
|
||||
<view
|
||||
class="export-option-item"
|
||||
:class="{ selected: selectedExportFormat === 'pdf' }"
|
||||
@click="selectExportFormat('pdf')"
|
||||
>
|
||||
<view class="option-radio">
|
||||
<view class="radio-dot" v-if="selectedExportFormat === 'pdf'"></view>
|
||||
</view>
|
||||
<text class="option-text">PDF 报告(含证书图片)</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="export-modal-footer">
|
||||
<view class="export-modal-btn cancel-btn" @click="closeExportDialog">取消</view>
|
||||
<view class="export-modal-btn confirm-btn" @click="confirmExport">确认导出</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- PDF预览组件 -->
|
||||
<PdfPreview
|
||||
:visible="showPdfPreview"
|
||||
:pdfUrl="pdfPreviewUrl"
|
||||
@update:visible="showPdfPreview = $event"
|
||||
@close="showPdfPreview = false"
|
||||
/>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, reactive } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { jfSummaryByJfTypeApi } from "@/api/base/jfApi";
|
||||
import { jfSummaryByJfTypeApi, jfExportApi } from "@/api/base/jfApi";
|
||||
import { imagUrl } from "@/utils";
|
||||
import { BASE_URL, AUTH_KEY } from "@/config";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import PdfPreview from "@/pages/components/PdfPreview/index.vue";
|
||||
|
||||
interface EvaluationItem {
|
||||
id: string;
|
||||
@ -97,14 +149,23 @@ const evaluationItems = ref<EvaluationItem[]>([]);
|
||||
const totalScore = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 时间筛选(默认当年)
|
||||
// 导出相关
|
||||
const showExportModal = ref(false);
|
||||
const selectedExportFormat = ref<'excel' | 'pdf'>('excel');
|
||||
|
||||
// PDF预览相关
|
||||
const showPdfPreview = ref(false);
|
||||
const pdfPreviewUrl = ref('');
|
||||
|
||||
// 时间筛选(默认最近两年)
|
||||
const currentYear = new Date().getFullYear();
|
||||
const twoYearsAgo = currentYear - 1; // 两年前(去年)
|
||||
const formatDate = (date: Date) =>
|
||||
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
|
||||
const searchForm = reactive({
|
||||
startTime: `${currentYear}-01-01`,
|
||||
endTime: `${currentYear}-12-31`,
|
||||
startTime: `${twoYearsAgo}-01-01`, // 两年前的1月1日
|
||||
endTime: `${currentYear}-12-31`, // 当前年的12月31日
|
||||
});
|
||||
|
||||
const onStartTimeChange = (e: any) => {
|
||||
@ -116,8 +177,8 @@ const onEndTimeChange = (e: any) => {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.startTime = `${currentYear}-01-01`;
|
||||
searchForm.endTime = `${currentYear}-12-31`;
|
||||
searchForm.startTime = `${twoYearsAgo}-01-01`; // 重置为两年前的1月1日
|
||||
searchForm.endTime = `${currentYear}-12-31`; // 重置为当前年的12月31日
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
@ -258,6 +319,383 @@ function handleIntegralApply() {
|
||||
});
|
||||
}
|
||||
|
||||
// 跳转到明细页面
|
||||
const handleGoToDetail = () => {
|
||||
// 从本地缓存获取当前教师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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jsId) {
|
||||
uni.showToast({
|
||||
title: '未找到教师信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳转到明细页面(不传 jfTypeId 表示查看所有类别的明细)
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/JiFenPingJia/jfself/ScoreListByType?jsId=${jsId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 显示导出格式选择弹窗
|
||||
const showExportDialog = () => {
|
||||
showExportModal.value = true;
|
||||
selectedExportFormat.value = 'excel'; // 默认选择 Excel
|
||||
};
|
||||
|
||||
// 关闭导出弹窗
|
||||
const closeExportDialog = () => {
|
||||
showExportModal.value = false;
|
||||
selectedExportFormat.value = 'excel';
|
||||
};
|
||||
|
||||
// 选择导出格式
|
||||
const selectExportFormat = (format: 'excel' | 'pdf') => {
|
||||
selectedExportFormat.value = format;
|
||||
};
|
||||
|
||||
// 确认导出
|
||||
const confirmExport = () => {
|
||||
// 先保存选择的值,再关闭弹窗(因为 closeExportDialog 会重置 selectedExportFormat)
|
||||
const format = selectedExportFormat.value;
|
||||
closeExportDialog();
|
||||
|
||||
console.log('[导出] 用户选择的格式:', format);
|
||||
|
||||
if (format === 'excel') {
|
||||
console.log('[导出] 执行 Excel 导出');
|
||||
handleExportExcel();
|
||||
} else if (format === 'pdf') {
|
||||
console.log('[导出] 执行 PDF 导出');
|
||||
handleExportPdf();
|
||||
} else {
|
||||
console.error('[导出] 未知的导出格式:', format);
|
||||
uni.showToast({
|
||||
title: '未知的导出格式',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 导出 Excel
|
||||
const handleExportExcel = async () => {
|
||||
try {
|
||||
// 从本地缓存获取当前教师ID
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
let jsId = '';
|
||||
let userData = null;
|
||||
|
||||
if (userDataStr) {
|
||||
try {
|
||||
userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
jsId = userData.jsData.id;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未找到教师信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '用户数据解析失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未找到用户数据',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[导出] 步骤1: 开始导出');
|
||||
uni.showLoading({ title: '导出中...' });
|
||||
|
||||
console.log('[导出] 步骤2: 调用后端接口, jsId:', jsId, 'startTime:', searchForm.startTime, 'endTime:', searchForm.endTime);
|
||||
|
||||
// 调用导出接口
|
||||
// 传递教师ID和时间范围(按获奖时间筛选)
|
||||
const result: any = await jfExportApi({
|
||||
fileds: '', // 空字符串(已废弃,使用DTO固定字段)
|
||||
jsId: jsId, // 教师ID(根据当前教师过滤)
|
||||
startTime: searchForm.startTime, // 开始时间(按获奖时间筛选)
|
||||
endTime: searchForm.endTime // 结束时间(按获奖时间筛选)
|
||||
});
|
||||
|
||||
console.log('[导出] 步骤3: 接口返回, result:', result);
|
||||
console.log('[导出] 步骤3.1: result.resultCode =', result.resultCode, ', result.result =', result.result);
|
||||
|
||||
// 关闭加载提示
|
||||
uni.hideLoading();
|
||||
console.log('[导出] 步骤4: 已关闭loading');
|
||||
|
||||
// AjaxResult 返回的文件路径在 msg 字段中,ReturnMsg 返回的在 result 字段中
|
||||
// 兼容两种格式
|
||||
const filePath = result.msg || result.result;
|
||||
if ((result.resultCode === 1 || result.resultCode === 0) && filePath) {
|
||||
console.log('[导出] 步骤5: 验证通过,开始处理下载');
|
||||
|
||||
// 后端返回文件路径(AjaxResult 在 msg 字段,ReturnMsg 在 result 字段)
|
||||
console.log('[导出] 步骤6: 文件路径:', filePath);
|
||||
|
||||
// 构建完整的下载URL
|
||||
console.log('[导出] 步骤7: 调用imagUrl前, filePath:', filePath);
|
||||
const downloadUrl = filePath.startsWith('http') ? filePath : imagUrl(filePath);
|
||||
console.log('[导出] 步骤8: 下载URL构建完成:', downloadUrl);
|
||||
|
||||
// 提示用户
|
||||
console.log('[导出] 步骤10: 显示下载提示');
|
||||
uni.showToast({
|
||||
title: '正在下载Excel文件...',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
console.log('[导出] 步骤11: 准备触发下载');
|
||||
// 延迟触发下载,确保toast显示
|
||||
setTimeout(() => {
|
||||
console.log('[导出] 步骤12: setTimeout执行,准备下载');
|
||||
// 判断平台并执行相应的下载逻辑
|
||||
// @ts-ignore
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
// H5平台:直接使用window.location.href触发下载
|
||||
console.log('[导出] 步骤13: H5环境,触发下载');
|
||||
window.location.href = downloadUrl;
|
||||
console.log('[导出] 步骤14: window.location.href已执行');
|
||||
} else {
|
||||
// APP/小程序环境:使用uni.downloadFile
|
||||
console.log('[导出] 步骤13: APP/小程序环境,使用uni.downloadFile');
|
||||
uni.downloadFile({
|
||||
url: downloadUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('[导出] Excel下载成功,临时文件路径:', res.tempFilePath);
|
||||
|
||||
// 保存文件到本地
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
console.log('[导出] Excel文件已保存到本地:', saveRes.savedFilePath);
|
||||
uni.showToast({
|
||||
title: '文件已保存',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[导出] 文件保存失败:', err);
|
||||
uni.showToast({
|
||||
title: '文件保存失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('[导出] 下载失败,状态码:', res.statusCode);
|
||||
uni.showToast({
|
||||
title: `下载失败(状态码: ${res.statusCode})`,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[导出] 下载Excel失败:', err);
|
||||
uni.showToast({
|
||||
title: '下载失败: ' + (err.errMsg || '网络异常'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
console.log('[导出] 步骤15: 主流程执行完毕');
|
||||
} else {
|
||||
console.log('[导出] 验证失败, resultCode:', result.resultCode, ', message:', result.message);
|
||||
uni.showToast({
|
||||
title: result.message || '导出失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[导出] catch捕获异常, error对象:', error);
|
||||
console.error('[导出] error类型:', typeof error);
|
||||
console.error('[导出] error详情:', JSON.stringify(error));
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '导出失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 导出 PDF
|
||||
const handleExportPdf = async () => {
|
||||
try {
|
||||
// 从本地缓存获取当前教师ID
|
||||
const userDataStr = uni.getStorageSync('app-user');
|
||||
let jsId = '';
|
||||
let userData = null;
|
||||
|
||||
if (userDataStr) {
|
||||
try {
|
||||
userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr;
|
||||
if (userData && userData.jsData && userData.jsData.id) {
|
||||
jsId = userData.jsData.id;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未找到教师信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '用户数据解析失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未找到用户数据',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[导出PDF] 步骤1: 开始导出PDF');
|
||||
uni.showLoading({ title: '正在生成PDF...' });
|
||||
|
||||
console.log('[导出PDF] 步骤2: 调用后端接口, jsId:', jsId, 'startTime:', searchForm.startTime, 'endTime:', searchForm.endTime);
|
||||
|
||||
// 构建 PDF 生成 URL
|
||||
const userStore = useUserStore();
|
||||
const pdfApiUrl = `${BASE_URL}/api/jf/exportPdf?jsId=${jsId}&startTime=${searchForm.startTime}&endTime=${searchForm.endTime}`;
|
||||
|
||||
console.log('[导出PDF] PDF生成URL:', pdfApiUrl);
|
||||
|
||||
// #ifdef H5
|
||||
// H5环境:使用fetch下载PDF到blob,然后使用PDF.js预览
|
||||
try {
|
||||
console.log('[导出PDF] H5环境:开始下载PDF用于预览');
|
||||
|
||||
const response = await fetch(pdfApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
[AUTH_KEY]: userStore.getToken
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器返回错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
console.log('[导出PDF] PDF数据接收成功,大小:', (blob.size / 1024 / 1024).toFixed(2), 'MB');
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
console.log('[导出PDF] Blob URL创建成功:', blobUrl);
|
||||
|
||||
// 使用项目中的 PDF.js 查看器预览
|
||||
const viewerPath = '/static/system/pdfReader/index.html';
|
||||
const previewUrl = `${viewerPath}?file=${encodeURIComponent(blobUrl)}#pagemode=none`;
|
||||
console.log('[导出PDF] PDF查看器URL:', previewUrl);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
// 使用组件预览PDF
|
||||
console.log('[导出PDF] 设置预览URL:', previewUrl);
|
||||
pdfPreviewUrl.value = previewUrl;
|
||||
console.log('[导出PDF] pdfPreviewUrl.value 已设置:', pdfPreviewUrl.value);
|
||||
// 使用 nextTick 确保响应式更新
|
||||
setTimeout(() => {
|
||||
showPdfPreview.value = true;
|
||||
console.log('[导出PDF] showPdfPreview.value 已设置为 true:', showPdfPreview.value);
|
||||
}, 100);
|
||||
} catch (error: any) {
|
||||
console.error('[导出PDF] H5环境下载失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载失败: ' + (error?.message || '网络异常'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// APP/小程序环境:使用uni.downloadFile
|
||||
uni.downloadFile({
|
||||
url: pdfApiUrl,
|
||||
header: {
|
||||
[AUTH_KEY]: userStore.getToken
|
||||
},
|
||||
success: (res) => {
|
||||
uni.hideLoading();
|
||||
if (res.statusCode === 200) {
|
||||
console.log('[导出PDF] PDF下载成功,临时文件路径:', res.tempFilePath);
|
||||
|
||||
// 使用PDF.js查看器预览(通过web-view)
|
||||
const viewerPath = '/static/system/pdfReader/index.html';
|
||||
// 对于APP环境,需要将文件路径转换为可访问的URL
|
||||
// 这里使用file://协议或者通过web-view直接访问
|
||||
const previewUrl = `${viewerPath}?file=${encodeURIComponent(res.tempFilePath)}#pagemode=none`;
|
||||
|
||||
// 使用组件预览PDF
|
||||
console.log('[导出PDF] APP环境设置预览URL:', previewUrl);
|
||||
pdfPreviewUrl.value = previewUrl;
|
||||
console.log('[导出PDF] pdfPreviewUrl.value 已设置:', pdfPreviewUrl.value);
|
||||
// 使用 nextTick 确保响应式更新
|
||||
setTimeout(() => {
|
||||
showPdfPreview.value = true;
|
||||
console.log('[导出PDF] showPdfPreview.value 已设置为 true:', showPdfPreview.value);
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('[导出PDF] 下载失败,状态码:', res.statusCode);
|
||||
uni.showToast({
|
||||
title: `下载失败(状态码: ${res.statusCode})`,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[导出PDF] 下载PDF失败:', err);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载失败: ' + (err.errMsg || '网络异常'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('[导出PDF] catch捕获异常:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '导出失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
loadTeacherScore();
|
||||
@ -307,6 +745,40 @@ onShow(() => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #1890ff;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
font-size: 14px;
|
||||
color: #52c41a;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #52c41a;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background-color: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -471,4 +943,144 @@ onShow(() => {
|
||||
box-shadow: 0 0 0 0 rgba(255, 107, 53, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 导出弹窗样式 */
|
||||
.export-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.export-modal-content {
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.export-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.export-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.export-modal-close {
|
||||
font-size: 28px;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.export-modal-body {
|
||||
padding: 20px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.export-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.selected {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.option-radio {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #d9d9d9;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.export-option-item.selected & {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.export-modal-footer {
|
||||
display: flex;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.export-modal-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
|
||||
&:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background-color: #0050b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -37,6 +37,13 @@
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 审核中状态显示撤回按钮 -->
|
||||
<view v-if="isPendingReview(item.spJd, item.spResult) && item.spResult !== 'C'" class="card-actions-header">
|
||||
<button class="withdraw-btn-header" @click="handleWithdraw(item)">
|
||||
撤回申请
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="item-content">
|
||||
<!-- 业绩类别区域 -->
|
||||
<view class="section">
|
||||
@ -163,7 +170,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 驳回原因列表 -->
|
||||
<view v-if="item.jfStatus === 'C' && item.rejectionReasons && item.rejectionReasons.length > 0" class="rejection-reasons-section">
|
||||
<view v-if="item.spResult === 'C' && item.rejectionReasons && item.rejectionReasons.length > 0" class="rejection-reasons-section">
|
||||
<view class="section-title-rejection">
|
||||
<view class="title-line-rejection"></view>
|
||||
<text class="title-text-rejection">驳回原因</text>
|
||||
@ -207,7 +214,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { jfFindPageApi, jfTypeStructureApi } from "@/api/base/jfApi";
|
||||
import { jfFindPageApi, jfTypeStructureApi, jfWithdrawApi } from "@/api/base/jfApi";
|
||||
|
||||
interface ScoreItem {
|
||||
id?: string;
|
||||
@ -372,19 +379,29 @@ const formatScoreConfig = (config: any): ScoreConfigCategory[] => {
|
||||
// 解析驳回原因 JSON 字符串
|
||||
function parseRejectionReasons(jsonStr: string | null | undefined): Array<{
|
||||
userName: string;
|
||||
approveTime: string;
|
||||
approveTime: string | null;
|
||||
approveRemark: string;
|
||||
}> {
|
||||
if (!jsonStr) return [];
|
||||
try {
|
||||
let parsed: any[] = [];
|
||||
// 如果是字符串,尝试解析为 JSON
|
||||
if (typeof jsonStr === 'string') {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
parsed = JSON.parse(jsonStr);
|
||||
} else if (Array.isArray(jsonStr)) {
|
||||
// 如果已经是数组,直接使用
|
||||
parsed = jsonStr;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
// 如果已经是数组,直接返回
|
||||
if (Array.isArray(jsonStr)) {
|
||||
return jsonStr;
|
||||
|
||||
// 确保返回的是数组,并处理 null 值
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map(item => ({
|
||||
userName: item.userName || '',
|
||||
approveTime: item.approveTime || null,
|
||||
approveRemark: item.approveRemark || ''
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
@ -445,6 +462,66 @@ function handleResubmit(item: ScoreItem) {
|
||||
});
|
||||
}
|
||||
|
||||
// 处理撤回
|
||||
async function handleWithdraw(item: ScoreItem) {
|
||||
if (!item.id) {
|
||||
uni.showToast({
|
||||
title: '缺少积分ID',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认撤回',
|
||||
content: '确定要撤回该积分申请吗?撤回后将无法恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '撤回中...' });
|
||||
const result: any = await jfWithdrawApi({
|
||||
id: item.id,
|
||||
spRemark: '申请人撤回'
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
console.log('撤回接口返回结果:', result);
|
||||
console.log('resultCode:', result?.resultCode, 'success:', result?.success, 'message:', result?.message);
|
||||
|
||||
// 判断成功:resultCode === 1 或 success === true
|
||||
if (result && (result.resultCode === 1 || result.resultCode === 0 || result.success === true)) {
|
||||
uni.showToast({
|
||||
title: result.message || '撤回成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
// 延迟返回,确保提示信息显示
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
}, 500);
|
||||
} else {
|
||||
console.warn('撤回失败,返回结果:', result);
|
||||
uni.showToast({
|
||||
title: result?.message || result?.msg || '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('撤回失败,异常信息:', error);
|
||||
uni.showToast({
|
||||
title: '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
function previewFile(item: ScoreItem) {
|
||||
if (item.fileUrl) {
|
||||
@ -1060,6 +1137,25 @@ onLoad(async (options) => {
|
||||
background: linear-gradient(135deg, #d81b24 0%, #f5222d 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-btn-header {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: linear-gradient(135deg, #ff9800 0%, #ffa726 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 152, 0, 0.35);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: linear-gradient(135deg, #f57c00 0%, #ff9800 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审批状态徽章动画
|
||||
|
||||
@ -79,12 +79,24 @@
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
|
||||
<!-- 底部返回按钮 -->
|
||||
<view class="bottom-section">
|
||||
<view class="bottom-btn-wrapper">
|
||||
<u-button
|
||||
text="返回"
|
||||
type="primary"
|
||||
class="back-btn"
|
||||
@click="handleBack"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/Search.vue";
|
||||
import { jfFindPageApi, jfTypeStructureApi } from "@/api/base/jfApi";
|
||||
@ -170,9 +182,13 @@ const queryData = async (pageNo: number, pageSize: number) => {
|
||||
const params: any = {
|
||||
page: pageNo,
|
||||
rows: pageSize,
|
||||
jfTypeIds: jfTypeIdsParam,
|
||||
jsId: jsId.value,
|
||||
};
|
||||
|
||||
// 只有当 jfTypeIdsParam 有值时才添加该参数(查看所有明细时不传)
|
||||
if (jfTypeIdsParam) {
|
||||
params.jfTypeIds = jfTypeIdsParam;
|
||||
}
|
||||
const res: any = await jfFindPageApi(params);
|
||||
const result = res?.data || res?.result || res;
|
||||
let rows: any[] = [];
|
||||
@ -245,6 +261,11 @@ const loadAllJfTypeIds = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 返回按钮处理
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
onLoad((options: any) => {
|
||||
jfTypeId.value = options?.jfTypeId || "";
|
||||
jsId.value = options?.jsId || "";
|
||||
@ -253,6 +274,13 @@ onLoad((options: any) => {
|
||||
pagingRef.value?.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// 页面显示时刷新列表(用于从详情页返回时刷新)
|
||||
onShow(() => {
|
||||
if (pagingRef.value) {
|
||||
pagingRef.value.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -299,12 +327,20 @@ onLoad((options: any) => {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
min-width: 68px;
|
||||
height: 40px;
|
||||
|
||||
// 使用深度选择器确保按钮高度与输入框一致
|
||||
:deep(.u-btn) {
|
||||
height: 40px !important;
|
||||
line-height: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
margin-top: 80px;
|
||||
height: calc(100vh - 80px);
|
||||
margin-bottom: 70px; // 为底部按钮留出空间
|
||||
height: calc(100vh - 80px - 70px); // 减去顶部和底部高度
|
||||
padding: 12px;
|
||||
padding-bottom: 16px;
|
||||
position: relative;
|
||||
@ -469,5 +505,27 @@ onLoad((options: any) => {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 底部按钮区域
|
||||
.bottom-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.bottom-btn-wrapper {
|
||||
max-width: 100%;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -452,9 +452,18 @@ const handleStop = () => {
|
||||
// 跳转回审批列表页面
|
||||
const navigateToIndex = () => {
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/view/routine/JiFenPingJia/jfsp/index'
|
||||
});
|
||||
// 使用 navigateBack 返回上一页,保留查询条件
|
||||
// 如果上一页不存在(直接打开详情页),则使用 redirectTo
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url: '/pages/view/routine/JiFenPingJia/jfsp/index'
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
|
||||
1397
src/pages/view/routine/JiFenPingJia/jfsp/compare.vue
Normal file
1397
src/pages/view/routine/JiFenPingJia/jfsp/compare.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,17 @@
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-filter">
|
||||
<view class="category-selector" @click="openJfTypePicker">
|
||||
<text class="category-text" :class="{ 'placeholder': !selectedJfTypeName }">
|
||||
{{ selectedJfTypeName || '选择业绩类别' }}
|
||||
</text>
|
||||
<uni-icons type="right" size="16" color="#999" />
|
||||
<view v-if="selectedJfTypeId" class="clear-btn" @click.stop="clearJfType">
|
||||
<uni-icons type="closeempty" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="tab in filterTabs"
|
||||
@ -33,6 +44,19 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 业绩类别树形选择器 -->
|
||||
<BasicTree
|
||||
ref="basicTreeRef"
|
||||
:range="jfTypeTree"
|
||||
:idKey="'id'"
|
||||
:rangeKey="'jfmc'"
|
||||
title="选择业绩类别"
|
||||
:multiple="false"
|
||||
:selectParent="false"
|
||||
:defaultExpandLevel="0"
|
||||
@confirm="handleJfTypeConfirm"
|
||||
/>
|
||||
|
||||
<view class="middle-section">
|
||||
<z-paging
|
||||
ref="pagingRef"
|
||||
@ -86,11 +110,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/Search.vue";
|
||||
import { jfFindUserTodosPageApi } from "@/api/base/jfApi";
|
||||
import BasicTree from "@/components/BasicTree/Tree.vue";
|
||||
import { jfFindUserTodosPageApi, jfTypeFindPageApi } from "@/api/base/jfApi";
|
||||
import { navigateTo } from "@/utils/uniapp";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
@ -100,12 +125,54 @@ const filterTabs = [
|
||||
{ key: "all", label: "全部" },
|
||||
];
|
||||
|
||||
// 本地存储的 key
|
||||
const STORAGE_KEY = 'jfsp_search_conditions';
|
||||
|
||||
// 查询条件状态
|
||||
const activeTab = ref("pending");
|
||||
const searchKeyword = ref("");
|
||||
const dataList = ref<any[]>([]);
|
||||
const pagingRef = ref<any>(null);
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 业绩类别相关
|
||||
const selectedJfTypeId = ref<string>("");
|
||||
const selectedJfTypeName = ref<string>("");
|
||||
const jfTypeTree = ref<any[]>([]);
|
||||
const basicTreeRef = ref<any>(null);
|
||||
|
||||
// 保存查询条件到本地存储
|
||||
const saveSearchConditions = () => {
|
||||
try {
|
||||
const conditions = {
|
||||
activeTab: activeTab.value,
|
||||
searchKeyword: searchKeyword.value,
|
||||
selectedJfTypeId: selectedJfTypeId.value,
|
||||
selectedJfTypeName: selectedJfTypeName.value,
|
||||
};
|
||||
uni.setStorageSync(STORAGE_KEY, conditions);
|
||||
} catch (error) {
|
||||
console.error('保存查询条件失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储恢复查询条件
|
||||
const restoreSearchConditions = () => {
|
||||
try {
|
||||
const conditions = uni.getStorageSync(STORAGE_KEY);
|
||||
if (conditions) {
|
||||
activeTab.value = conditions.activeTab || "pending";
|
||||
searchKeyword.value = conditions.searchKeyword || "";
|
||||
selectedJfTypeId.value = conditions.selectedJfTypeId || "";
|
||||
selectedJfTypeName.value = conditions.selectedJfTypeName || "";
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复查询条件失败:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getCurrentTeacherId = () => {
|
||||
const jsData = userStore.getJs;
|
||||
return jsData?.id || null;
|
||||
@ -113,11 +180,13 @@ const getCurrentTeacherId = () => {
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
searchKeyword.value = keyword;
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
const switchTab = (tabKey: string) => {
|
||||
activeTab.value = tabKey;
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
@ -142,17 +211,34 @@ const queryData = async (pageNo: number, pageSize: number) => {
|
||||
}
|
||||
|
||||
let response: any;
|
||||
const params: any = {
|
||||
jsId,
|
||||
page: pageNo,
|
||||
rows: pageSize,
|
||||
jfTypeId: selectedJfTypeId.value || "",
|
||||
};
|
||||
|
||||
if (activeTab.value === "all") {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "", jsId, spType: "", page: pageNo, rows: pageSize });
|
||||
params.dbZt = "";
|
||||
params.spType = "";
|
||||
params.spJd = "";
|
||||
} else if (activeTab.value === "pending") {
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "A", jsId, spType: "SP", page: pageNo, rows: pageSize });
|
||||
params.dbZt = "A";
|
||||
params.spType = "SP";
|
||||
params.spJd = "A";
|
||||
} else if (activeTab.value === "approved") {
|
||||
// 为了让驳回数据也出现在已办列表,这里不过滤 dbZt,后续用前端筛选
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "", jsId, spType: "SP", page: pageNo, rows: pageSize });
|
||||
params.dbZt = "";
|
||||
params.spType = "SP";
|
||||
params.spJd = "Z";
|
||||
} else {
|
||||
// 默认全部(不含抄送)
|
||||
response = await jfFindUserTodosPageApi({ dbZt: "", jsId, spType: "SP", page: pageNo, rows: pageSize });
|
||||
params.dbZt = "";
|
||||
params.spType = "SP";
|
||||
params.spJd = "";
|
||||
}
|
||||
|
||||
response = await jfFindUserTodosPageApi(params);
|
||||
|
||||
console.log("积分审批列表接口返回 raw:", response);
|
||||
const result = response?.data || response;
|
||||
@ -232,8 +318,132 @@ const goToDetail = (item: any) => {
|
||||
navigateTo(`/pages/view/routine/JiFenPingJia/jfsp/JfFlow?id=${id}&from=db&dbZt=${dbZt}`);
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
// 构建树形结构
|
||||
function buildTree(list: any[]): any[] {
|
||||
const map = new Map();
|
||||
const roots: any[] = [];
|
||||
|
||||
list.forEach(item => {
|
||||
map.set(item.id, {
|
||||
...item,
|
||||
children: []
|
||||
});
|
||||
});
|
||||
|
||||
list.forEach(item => {
|
||||
const node = map.get(item.id);
|
||||
if (item.pid && map.has(item.pid)) {
|
||||
const parent = map.get(item.pid);
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
function cleanEmptyChildren(nodes: any[]) {
|
||||
nodes.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
cleanEmptyChildren(node.children);
|
||||
} else {
|
||||
delete node.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanEmptyChildren(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 根据 ID 查找树节点并获取完整路径
|
||||
function findNodeById(nodes: any[], id: string, path: string[] = []): { node: any, path: string[] } | null {
|
||||
for (const node of nodes) {
|
||||
const currentPath = [...path, node.jfmc];
|
||||
if (String(node.id) === String(id)) {
|
||||
return { node, path: currentPath };
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const found = findNodeById(node.children, id, currentPath);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 加载业绩类别数据
|
||||
const loadJfTypes = async () => {
|
||||
try {
|
||||
const res: any = await jfTypeFindPageApi({
|
||||
page: 1,
|
||||
rows: 1000
|
||||
});
|
||||
const list = res?.result?.rows || res?.rows || res?.result || [];
|
||||
const treeData = buildTree(list);
|
||||
jfTypeTree.value = treeData;
|
||||
} catch (error) {
|
||||
console.error('加载业绩类别失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开类别选择器
|
||||
const openJfTypePicker = () => {
|
||||
if (basicTreeRef.value && basicTreeRef.value._show) {
|
||||
basicTreeRef.value._show();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理类别选择确认
|
||||
const handleJfTypeConfirm = (value: any) => {
|
||||
// value 是选中的节点数组,每个节点包含 source(原始数据)和 parents(父级数组)
|
||||
if (value && value.length > 0) {
|
||||
const selectedNode = value[0];
|
||||
const source = selectedNode.source || selectedNode;
|
||||
const jfTypeId = source.id || source[basicTreeRef.value?.idKey || 'id'];
|
||||
selectedJfTypeId.value = String(jfTypeId);
|
||||
|
||||
// 构建路径显示:使用 parents 数组构建完整路径
|
||||
if (selectedNode.parents && selectedNode.parents.length > 0) {
|
||||
const pathParts = selectedNode.parents.map((p: any) => p[basicTreeRef.value?.rangeKey || 'jfmc'] || p.jfmc || p.name);
|
||||
pathParts.push(source[basicTreeRef.value?.rangeKey || 'jfmc'] || source.jfmc || source.name);
|
||||
selectedJfTypeName.value = pathParts.join(' / ');
|
||||
} else {
|
||||
// 如果没有 parents,尝试从树中查找
|
||||
const found = findNodeById(jfTypeTree.value, jfTypeId);
|
||||
if (found) {
|
||||
selectedJfTypeName.value = found.path.join(' / ');
|
||||
} else {
|
||||
selectedJfTypeName.value = source[basicTreeRef.value?.rangeKey || 'jfmc'] || source.jfmc || source.name || '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearJfType();
|
||||
}
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
// 清除类别选择
|
||||
const clearJfType = () => {
|
||||
selectedJfTypeId.value = '';
|
||||
selectedJfTypeName.value = '';
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadJfTypes();
|
||||
// 页面加载时恢复查询条件
|
||||
restoreSearchConditions();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
// 如果页面被重新初始化(如使用 redirectTo),恢复查询条件
|
||||
const hasRestored = restoreSearchConditions();
|
||||
// 恢复条件后需要重新加载数据
|
||||
if (hasRestored || pagingRef.value) {
|
||||
pagingRef.value?.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -282,6 +492,47 @@ onShow(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.category-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
padding: 2px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@ -308,8 +559,8 @@ onShow(() => {
|
||||
|
||||
.middle-section {
|
||||
flex: 1;
|
||||
margin-top: 140px; // 为顶部搜索栏留出空间
|
||||
height: calc(100vh - 140px); // 减去顶部区域的高度
|
||||
margin-top: 200px; // 为顶部搜索栏和类别选择器留出空间
|
||||
height: calc(100vh - 200px); // 减去顶部区域的高度
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
458
src/pages/view/routine/JiFenPingJia/jfsp/list.vue
Normal file
458
src/pages/view/routine/JiFenPingJia/jfsp/list.vue
Normal file
@ -0,0 +1,458 @@
|
||||
<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"
|
||||
class="paging-container"
|
||||
@query="queryData"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in dataList"
|
||||
:key="item.applicantId || index"
|
||||
class="applicant-card"
|
||||
@click="goToCompare(item)"
|
||||
>
|
||||
<view class="card-header">
|
||||
<view class="applicant-info">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ getAvatarText(item.applicantName) }}</text>
|
||||
</view>
|
||||
<view class="name-section">
|
||||
<text class="applicant-name">{{ item.applicantName || "—" }}</text>
|
||||
<text class="applicant-dept" v-if="item.applicantDept">{{ item.applicantDept }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="badge" v-if="item.recordCount > 0">
|
||||
<text class="badge-text">{{ item.recordCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">待审批:</text>
|
||||
<text class="stat-value highlight">{{ item.recordCount || 0 }}条</text>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">总积分:</text>
|
||||
<text class="stat-value score">+{{ item.totalScore || 0 }}分</text>
|
||||
</view>
|
||||
<view class="stat-row" v-if="item.earliestTime || item.latestTime">
|
||||
<text class="stat-label">时间范围:</text>
|
||||
<text class="stat-value">{{ formatTimeRange(item.earliestTime, item.latestTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="action-text">
|
||||
审批
|
||||
<uni-icons type="arrowright" size="14" color="#007aff"></uni-icons>
|
||||
</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 { jfFindUserTodosGroupByApplicantApi } from "@/api/base/jfApi";
|
||||
import { navigateTo } from "@/utils/uniapp";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const filterTabs = [
|
||||
{ key: "pending", label: "待办" },
|
||||
{ key: "approved", 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 filterData = (list: any[]) => {
|
||||
let filtered = list;
|
||||
if (searchKeyword.value) {
|
||||
const kw = searchKeyword.value.toLowerCase();
|
||||
filtered = filtered.filter((item) =>
|
||||
(item.applicantName || "").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 dbZt = "";
|
||||
let spType = "SP";
|
||||
|
||||
if (activeTab.value === "pending") {
|
||||
dbZt = "A";
|
||||
spType = "SP";
|
||||
} else if (activeTab.value === "approved") {
|
||||
dbZt = "B";
|
||||
spType = "SP";
|
||||
} else if (activeTab.value === "all") {
|
||||
dbZt = "";
|
||||
spType = "";
|
||||
}
|
||||
|
||||
const response: any = await jfFindUserTodosGroupByApplicantApi({
|
||||
jsId,
|
||||
dbZt,
|
||||
spType,
|
||||
page: pageNo,
|
||||
rows: pageSize,
|
||||
});
|
||||
|
||||
console.log("按申请人分组统计接口返回:", 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;
|
||||
} else if (Array.isArray(result)) {
|
||||
rows = result;
|
||||
}
|
||||
|
||||
const filtered = filterData(rows);
|
||||
console.log("过滤后的申请人列表:", filtered);
|
||||
|
||||
pagingRef.value?.complete(filtered);
|
||||
} catch (error) {
|
||||
console.error("加载申请人列表失败:", error);
|
||||
uni.showToast({ title: "加载失败", icon: "none" });
|
||||
pagingRef.value?.complete([]);
|
||||
}
|
||||
};
|
||||
|
||||
const getAvatarText = (name?: string) => {
|
||||
if (!name) return "?";
|
||||
// 取姓名的最后一个字符
|
||||
return name.charAt(name.length - 1);
|
||||
};
|
||||
|
||||
const formatTimeRange = (earliest?: string, latest?: string) => {
|
||||
if (!earliest && !latest) return "—";
|
||||
if (earliest && latest) {
|
||||
const e = dayjs(earliest).format("YYYY-MM");
|
||||
const l = dayjs(latest).format("YYYY-MM");
|
||||
if (e === l) return e;
|
||||
return `${e} ~ ${l}`;
|
||||
}
|
||||
return earliest ? dayjs(earliest).format("YYYY-MM") : dayjs(latest).format("YYYY-MM");
|
||||
};
|
||||
|
||||
const goToCompare = (item: any) => {
|
||||
if (!item.applicantId) {
|
||||
uni.showToast({ title: "缺少申请人ID", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
const dbZt = activeTab.value === 'approved' ? 'B' : activeTab.value === 'pending' ? 'A' : '';
|
||||
navigateTo(
|
||||
`/pages/view/routine/JiFenPingJia/jfsp/compare?applicantId=${item.applicantId}&from=db&dbZt=${dbZt}&mode=compare`
|
||||
);
|
||||
};
|
||||
|
||||
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: 20;
|
||||
background-color: #fff;
|
||||
padding: 10px 12px 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.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);
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.paging-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.applicant-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.applicant-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.name-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.applicant-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.applicant-dept {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.badge {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
background-color: #f43f5e;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-text {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.highlight {
|
||||
color: #007aff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.score {
|
||||
color: #fa8c16;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<view class="detail-section">
|
||||
<text class="section-title">工作描述</text>
|
||||
<view class="section-content">
|
||||
<rich-text class="detail-text" :nodes="detailData.gzms || '暂无描述'"></rich-text>
|
||||
<text class="detail-text">{{ detailData.gzms || '暂无描述' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -284,6 +284,7 @@ const goBack = () => {
|
||||
color: #5a6c7d;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
417
src/pages/view/routine/inv/invout/ApprovalOut.vue
Normal file
417
src/pages/view/routine/inv/invout/ApprovalOut.vue
Normal file
@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<view class="approval-out-container">
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<uni-icons type="spinner-cycle" size="24" color="#fff" class="loading-icon"></uni-icons>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<view class="search-card">
|
||||
<view class="search-item">
|
||||
<BasicSearch
|
||||
placeholder="搜索申请人或申请原因"
|
||||
v-model="searchKeyword"
|
||||
class="search-input"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<u-button
|
||||
text="查询"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="search-button"
|
||||
@click="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<scroll-view scroll-y class="list-scroll">
|
||||
<view v-if="loading && applyList.length === 0" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="applyList.length === 0" class="empty-container">
|
||||
<text class="empty-text">暂无已审批的申请记录</text>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="(item, index) in applyList"
|
||||
:key="item.id || index"
|
||||
class="apply-card"
|
||||
@click="handleSelectApply(item)"
|
||||
>
|
||||
<view class="card-header">
|
||||
<text class="apply-title">{{ item.applyUserName || '—' }}的申请</text>
|
||||
<text class="apply-date">{{ formatDate(item.applyDate) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">申请原因:</text>
|
||||
<text class="value ellipsis">{{ item.applyReason || '—' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">总数量:</text>
|
||||
<text class="value qty">{{ item.totalQty || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<view class="action-text" @click.stop="handleViewDetail(item)">
|
||||
查看详情
|
||||
</view>
|
||||
<view class="action-text primary" @click.stop="handleCreateOut(item)">
|
||||
创建出库
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { invApplyFindPageApi, invApplyFindFullByIdApi } from '@/api/base/invApplyApi';
|
||||
import { invOutBillSaveApi } from '@/api/base/invOutBillApi';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { showToast } from '@/utils/uniapp';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { getJs } = useUserStore();
|
||||
|
||||
const applyList = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadingText = ref('加载中...');
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 查询已审批的申请(spResult='B' 且 spJd='Z')
|
||||
const loadApprovedApplies = async () => {
|
||||
loading.value = true;
|
||||
loadingText.value = '加载中...';
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
rows: 100, // 获取足够多的数据
|
||||
spResult: 'B', // 审批通过
|
||||
spJd: 'Z' // 审批完结
|
||||
};
|
||||
|
||||
const res: any = await invApplyFindPageApi(params);
|
||||
let list = res?.result?.rows || res?.rows || [];
|
||||
|
||||
// 前端再次筛选,确保是已审批通过的
|
||||
list = list.filter((item: any) =>
|
||||
item.spResult === 'B' && item.spJd === 'Z'
|
||||
);
|
||||
|
||||
// 如果有搜索关键词,进行过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
list = list.filter((item: any) => {
|
||||
const applyUserName = (item.applyUserName || '').toLowerCase();
|
||||
const applyReason = (item.applyReason || '').toLowerCase();
|
||||
return applyUserName.includes(keyword) || applyReason.includes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
applyList.value = list;
|
||||
} catch (error: any) {
|
||||
console.error('加载已审批申请失败:', error);
|
||||
showToast({
|
||||
title: '加载失败:' + (error?.message || '未知错误'),
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
loadApprovedApplies();
|
||||
};
|
||||
|
||||
const formatDate = (date: any) => {
|
||||
if (!date) return '—';
|
||||
return dayjs(date).format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
const handleViewDetail = (item: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/inv/invsq/detail?id=${item.id}`
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectApply = (item: any) => {
|
||||
// 点击卡片也可以创建出库
|
||||
handleCreateOut(item);
|
||||
};
|
||||
|
||||
const handleCreateOut = async (applyItem: any) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
loadingText.value = '创建出库单中...';
|
||||
|
||||
// 获取申请详情(包含明细)
|
||||
const res: any = await invApplyFindFullByIdApi({ id: applyItem.id });
|
||||
let data = null;
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
data = res.result;
|
||||
} else if (res && res.id) {
|
||||
data = res;
|
||||
}
|
||||
|
||||
if (!data || !data.items || data.items.length === 0) {
|
||||
showToast({
|
||||
title: '申请明细为空,无法创建出库单',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建出库单数据
|
||||
const js = getJs;
|
||||
const outBillData = {
|
||||
outType: 1, // 1-领用
|
||||
billDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
receiverUserId: data.applyUserId,
|
||||
handlerUserId: js?.id, // 经办人(当前用户)
|
||||
totalQty: data.totalQty || 0,
|
||||
totalAmount: 0, // 金额暂时为0
|
||||
billStatus: 1, // 已提交
|
||||
invOutBillItemDtos: data.items.map((item: any) => ({
|
||||
itemId: item.itemId,
|
||||
itemName: item.itemName,
|
||||
unitName: item.unitName,
|
||||
outQty: item.applyQty, // 出库数量等于申请数量
|
||||
warehouseId: item.warehouseId || '',
|
||||
locationId: item.locationId || ''
|
||||
}))
|
||||
};
|
||||
|
||||
// 调用保存接口
|
||||
const saveRes: any = await invOutBillSaveApi(outBillData);
|
||||
|
||||
if (saveRes && (saveRes.resultCode === 1 || saveRes.success === true)) {
|
||||
showToast({
|
||||
title: '出库单创建成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新列表
|
||||
setTimeout(() => {
|
||||
loadApprovedApplies();
|
||||
}, 500);
|
||||
} else {
|
||||
const errorMessage = saveRes?.message || saveRes?.resultMsg || '创建出库单失败,请重试';
|
||||
showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('创建出库单失败:', error);
|
||||
showToast({
|
||||
title: '创建出库单失败:' + (error?.message || '未知错误'),
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadApprovedApplies();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
// 页面显示时刷新数据
|
||||
loadApprovedApplies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.approval-out-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background-color: #ffffff;
|
||||
padding: 20rpx;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
|
||||
.search-card {
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 100rpx 0;
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
|
||||
.apply-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.apply-date {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
flex: 1;
|
||||
|
||||
&.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.qty {
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #e4e7ed;
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
padding: 8rpx 16rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载遮罩层样式
|
||||
.loading-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
min-width: 200rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
1146
src/pages/view/routine/inv/invout/DirectOut.vue
Normal file
1146
src/pages/view/routine/inv/invout/DirectOut.vue
Normal file
File diff suppressed because it is too large
Load Diff
96
src/pages/view/routine/inv/invout/index.vue
Normal file
96
src/pages/view/routine/inv/invout/index.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="invout-container">
|
||||
<!-- Tab切换 -->
|
||||
<view class="tab-container">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'approval' }"
|
||||
@click="switchTab('approval')"
|
||||
>
|
||||
<text class="tab-text">审批出库</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'direct' }"
|
||||
@click="switchTab('direct')"
|
||||
>
|
||||
<text class="tab-text">直接出库</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab内容区域 -->
|
||||
<view class="tab-content">
|
||||
<ApprovalOut v-if="activeTab === 'approval'" />
|
||||
<DirectOut v-if="activeTab === 'direct'" />
|
||||
</view>
|
||||
</view>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import ApprovalOut from './ApprovalOut.vue';
|
||||
import DirectOut from './DirectOut.vue';
|
||||
|
||||
const activeTab = ref<'approval' | 'direct'>('approval');
|
||||
|
||||
const switchTab = (tab: 'approval' | 'direct') => {
|
||||
activeTab.value = tab;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.invout-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.tab-text {
|
||||
font-size: 32rpx;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-text {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
617
src/pages/view/routine/inv/invsp/InvApplyApprove.vue
Normal file
617
src/pages/view/routine/inv/invsp/InvApplyApprove.vue
Normal file
@ -0,0 +1,617 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<!-- 统一的滚动容器,包含编辑组件和流程组件 -->
|
||||
<scroll-view scroll-y class="unified-scroll" :class="{ 'full-height': hideBottomBtn }">
|
||||
<!-- 领用申请详情展示 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">申请信息</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请人</text>
|
||||
<text class="info-value">{{ applyInfo.applyUserName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请日期</text>
|
||||
<text class="info-value">{{ formatDate(applyInfo.applyDate) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请原因</text>
|
||||
<text class="info-value">{{ applyInfo.applyReason || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">总数量</text>
|
||||
<text class="info-value">{{ applyInfo.totalQty || 0 }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">总金额</text>
|
||||
<text class="info-value">{{ applyInfo.totalAmount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请明细展示 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">申请明细</text>
|
||||
</view>
|
||||
|
||||
<view class="item-list" v-if="applyInfo.items && applyInfo.items.length > 0">
|
||||
<view
|
||||
v-for="(item, index) in applyInfo.items"
|
||||
:key="index"
|
||||
class="item-row"
|
||||
>
|
||||
<view class="item-header">
|
||||
<text class="item-index">第{{ index + 1 }}项</text>
|
||||
</view>
|
||||
|
||||
<view class="item-detail">
|
||||
<view class="info-item">
|
||||
<text class="info-label">物品</text>
|
||||
<text class="info-value">{{ item.itemName || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">单位</text>
|
||||
<text class="info-value">{{ item.unitName || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">数量</text>
|
||||
<text class="info-value">{{ item.applyQty || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-tip">
|
||||
<text>暂无明细数据</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批流程展示 -->
|
||||
<view class="flow-section" v-if="applyId">
|
||||
<LcglSp :yw-id="applyId" yw-type="INV_APPLY" :key="applyId" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<template #bottom>
|
||||
<view :class="{ 'hide-bottom': hideBottomBtn }">
|
||||
<YwConfirm
|
||||
v-if="showButton"
|
||||
:spApi="wrappedInvApplySpApi"
|
||||
:stopApi="wrappedInvApplyStopApi"
|
||||
:transferApi="wrappedInvApplyTransferApi"
|
||||
:params="spParams"
|
||||
:autoToMessage="false"
|
||||
:showXt="false"
|
||||
:showReject="!isYiban"
|
||||
:showTransfer="false"
|
||||
:showApprove="!isYiban"
|
||||
:showReturn="false"
|
||||
:showStop="true"
|
||||
:showXtDk="false"
|
||||
approveText="通过"
|
||||
@submit="handleSubmit"
|
||||
@reject="handleReject"
|
||||
@transfer="handleTransfer"
|
||||
@stop="handleStop"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { onLoad, onBackPress } from "@dcloudio/uni-app";
|
||||
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 {
|
||||
invApplyFindFullByIdApi,
|
||||
invApplySpApi,
|
||||
invApplyTransferApi,
|
||||
invApplyStopApi
|
||||
} from "@/api/base/invApplyApi";
|
||||
import { xxtsFindByIdApi } from "@/api/base/xxtsApi";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const { loginByOpenId, getJs } = useUserStore();
|
||||
|
||||
const applyId = ref<string>("");
|
||||
const applyInfo = ref<any>({});
|
||||
const showButton = ref<boolean>(false);
|
||||
const isLoginReady = ref<boolean>(false); // 登录是否准备就绪
|
||||
// 控制底部按钮区域显示/隐藏
|
||||
const hideBottomBtn = ref(false);
|
||||
// 遮罩层状态
|
||||
const loading = ref<boolean>(false);
|
||||
const loadingText = ref<string>("处理中...");
|
||||
// 数据加载状态
|
||||
const applyInfoLoaded = ref<boolean>(false); // 流程数据是否已加载
|
||||
// 是否已办(dbZt === "B" 表示已办)
|
||||
const dbZtFromUrl = ref<string>("");
|
||||
// 直接用一个 ref 保存 xxtsId
|
||||
const xxtsId = ref<string>("");
|
||||
const isYiban = computed(() => {
|
||||
return dbZtFromUrl.value === "B";
|
||||
});
|
||||
|
||||
// 审批参数(只包含审批相关字段,业务字段通过 save 接口保存)
|
||||
const spParams = computed(() => {
|
||||
return {
|
||||
xxtsId: xxtsId.value,
|
||||
ywId: applyId.value,
|
||||
ywType: 'INV_APPLY' // 领用申请类型
|
||||
};
|
||||
});
|
||||
|
||||
// 读取申请详情(用于审批时获取最新数据,包含申请信息和明细)
|
||||
const getApplyInfo = async () => {
|
||||
try {
|
||||
const res: any = await invApplyFindFullByIdApi({ id: applyId.value });
|
||||
|
||||
// 后端接口直接返回 InvApply 对象,也可能包装在 ReturnMsg 中
|
||||
let data = null;
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
// 包装在 ReturnMsg 中的情况
|
||||
data = res.result;
|
||||
} else if (res && res.id) {
|
||||
// 直接返回对象的情况
|
||||
data = res;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.error("获取领用申请详情失败: 数据为空");
|
||||
showButton.value = false;
|
||||
applyInfoLoaded.value = true;
|
||||
loading.value = false; // 关闭遮罩层
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置申请信息(包含明细数据)
|
||||
applyInfo.value = data;
|
||||
|
||||
// 判断是否显示审批按钮
|
||||
const spResult = data.spResult;
|
||||
const spJd = data.spJd;
|
||||
|
||||
// 已完结或驳回:隐藏审批按钮
|
||||
if (spResult === "C" || (spJd === "Z" && spResult === "B")) {
|
||||
showButton.value = false;
|
||||
} else if (spResult && spResult !== "A" && dbZtFromUrl.value === "A") {
|
||||
// 历史逻辑:当流程已完成且待办状态关闭时,不显示审批按钮
|
||||
showButton.value = false;
|
||||
} else {
|
||||
showButton.value = true;
|
||||
}
|
||||
|
||||
applyInfoLoaded.value = true;
|
||||
loading.value = false; // 关闭遮罩层
|
||||
} catch (error) {
|
||||
console.error("获取领用申请详情失败:", error);
|
||||
showButton.value = false;
|
||||
applyInfoLoaded.value = true;
|
||||
loading.value = false; // 关闭遮罩层
|
||||
}
|
||||
};
|
||||
|
||||
// 包装审批接口,先保存数据再审批
|
||||
const wrappedInvApplySpApi = async (params: any) => {
|
||||
console.log('领用申请审批接口被调用,参数:', params);
|
||||
|
||||
try {
|
||||
console.log('步骤1: 数据已保存,开始审批');
|
||||
loadingText.value = '正在审批...';
|
||||
|
||||
// 保存成功后,再调用审批接口(只传递审批相关参数,不传递业务字段)
|
||||
const spParams = {
|
||||
xxtsId: params.xxtsId,
|
||||
ywId: params.ywId,
|
||||
spStatus: params.spStatus,
|
||||
spRemark: params.spRemark,
|
||||
ywType: params.ywType
|
||||
};
|
||||
|
||||
console.log('步骤2: 调用审批接口,参数:', spParams);
|
||||
const result = await invApplySpApi(spParams);
|
||||
console.log('审批接口返回:', result);
|
||||
loading.value = false;
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('审批流程出错:', error);
|
||||
loading.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 包装转办接口
|
||||
const wrappedInvApplyTransferApi = async (params: any) => {
|
||||
console.log('领用申请转办接口被调用,参数:', params);
|
||||
|
||||
try {
|
||||
loadingText.value = '正在转办...';
|
||||
|
||||
// 调用转办接口
|
||||
const transferParams = {
|
||||
xxtsId: params.xxtsId,
|
||||
ywId: params.ywId,
|
||||
zbrIds: params.zbrIds,
|
||||
csrIds: params.csrIds,
|
||||
spRemark: params.spRemark,
|
||||
ywType: params.ywType
|
||||
};
|
||||
|
||||
console.log('调用转办接口,参数:', transferParams);
|
||||
const result = await invApplyTransferApi(transferParams);
|
||||
console.log('转办接口返回:', result);
|
||||
loading.value = false;
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('转办流程出错:', error);
|
||||
loading.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 包装终止接口
|
||||
const wrappedInvApplyStopApi = async (params: any) => {
|
||||
console.log('领用申请终止接口被调用,参数:', params);
|
||||
|
||||
try {
|
||||
loadingText.value = '正在终止...';
|
||||
|
||||
// 调用终止接口
|
||||
const stopParams = {
|
||||
xxtsId: params.xxtsId,
|
||||
ywId: params.ywId,
|
||||
spRemark: params.spRemark,
|
||||
ywType: params.ywType
|
||||
};
|
||||
|
||||
console.log('调用终止接口,参数:', stopParams);
|
||||
const result = await invApplyStopApi(stopParams);
|
||||
console.log('终止接口返回:', result);
|
||||
loading.value = false;
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('终止流程出错:', error);
|
||||
loading.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
// 处理审批操作完成后的跳转
|
||||
const handleSubmit = () => {
|
||||
navigateToIndex();
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
navigateToIndex();
|
||||
};
|
||||
|
||||
const handleTransfer = () => {
|
||||
navigateToIndex();
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
navigateToIndex();
|
||||
};
|
||||
|
||||
// 跳转回审批列表页面
|
||||
const navigateToIndex = () => {
|
||||
setTimeout(() => {
|
||||
// 发送刷新事件,通知列表页面刷新
|
||||
uni.$emit('refreshInvApplyList');
|
||||
|
||||
// 使用 navigateBack 返回上一页,保留查询条件
|
||||
// 如果上一页不存在(直接打开详情页),则使用 redirectTo
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url: '/pages/view/routine/inv/invsp/index'
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 返回按钮监听(仅 App 端生效,H5 端已在 ImagePreviewWithRotate 组件内部处理)
|
||||
onBackPress(() => {
|
||||
// 没有特殊处理,允许正常返回
|
||||
return false;
|
||||
});
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
if (!options || !options.id) {
|
||||
uni.showToast({ title: "缺少申请ID", icon: "none" });
|
||||
setTimeout(() => uni.navigateBack(), 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有 openId,先进行认证(防止 token 过期)
|
||||
if (options.openId) {
|
||||
try {
|
||||
const loginSuccess = await loginByOpenId(options.openId);
|
||||
if (!loginSuccess) {
|
||||
// 登录失败,停止后续流程
|
||||
return;
|
||||
}
|
||||
// 等待一下,确保 token 已保存到 store 中
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (e) {
|
||||
console.error("openId认证失败:", e);
|
||||
// 认证失败,停止后续流程
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记登录准备就绪,允许组件开始加载数据(无论是否有 openId)
|
||||
isLoginReady.value = true;
|
||||
|
||||
// 显示遮罩层
|
||||
loading.value = true;
|
||||
loadingText.value = '正在加载数据...';
|
||||
// 重置加载状态
|
||||
applyInfoLoaded.value = false;
|
||||
|
||||
let actualApplyId = options.id; // 默认使用传入的 ID
|
||||
|
||||
// 如果是从待办进入的(from=db),需要先获取 xxts 信息,然后获取业务 ID
|
||||
if (options.from === 'db') {
|
||||
try {
|
||||
// 从后端根据 url 中的 id(xxts ID)查询待办信息
|
||||
const xxtsRes = await xxtsFindByIdApi({ id: options.id });
|
||||
|
||||
if (xxtsRes && xxtsRes.result) {
|
||||
const xxts = xxtsRes.result;
|
||||
|
||||
// 直接保存 xxtsId
|
||||
if (xxts.id) {
|
||||
xxtsId.value = xxts.id;
|
||||
}
|
||||
|
||||
// 从 xxts 中获取业务 ID(xxzbId)
|
||||
if (xxts.xxzbId) {
|
||||
actualApplyId = xxts.xxzbId;
|
||||
}
|
||||
|
||||
// 从 xxts 获取 dbZt
|
||||
if (xxts.dbZt) {
|
||||
dbZtFromUrl.value = xxts.dbZt;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取待办信息失败:', error);
|
||||
// 如果获取失败,继续使用原始 ID
|
||||
}
|
||||
} else {
|
||||
// 如果不是从待办进入,检查 URL 参数中是否有 xxtsId
|
||||
if (options.xxtsId) {
|
||||
xxtsId.value = options.xxtsId;
|
||||
}
|
||||
|
||||
// 从 URL 参数获取 dbZt(如果传递了的话)
|
||||
if (options.dbZt) {
|
||||
dbZtFromUrl.value = options.dbZt;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用实际的业务 ID
|
||||
applyId.value = actualApplyId;
|
||||
|
||||
getApplyInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.unified-scroll {
|
||||
flex: 1;
|
||||
height: calc(100vh - 120rpx);
|
||||
overflow-y: auto;
|
||||
|
||||
&.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
.item-row {
|
||||
border: 1rpx solid #e4e7ed;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fafafa;
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
|
||||
.item-index {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
.info-item {
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx dashed #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flow-section {
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
margin-top: 30rpx;
|
||||
|
||||
// 确保流程组件与编辑组件有间距
|
||||
:deep(.lcgl-info) {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
// 流程组件的 section 也要移除左右边距
|
||||
:deep(.info-section) {
|
||||
border-radius: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一滚动容器的样式
|
||||
.unified-scroll {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
// 遮罩层样式
|
||||
.loading-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 60rpx 80rpx;
|
||||
min-width: 300rpx;
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 6rpx solid #f3f3f3;
|
||||
border-top: 6rpx solid #007aff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 隐藏底部按钮区域
|
||||
.hide-bottom {
|
||||
display: none !important;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
516
src/pages/view/routine/inv/invsp/index.vue
Normal file
516
src/pages/view/routine/inv/invsp/index.vue
Normal file
@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<view class="invsp-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"
|
||||
class="paging-container"
|
||||
@query="queryData"
|
||||
>
|
||||
<view
|
||||
v-for="(data, index) in dataList"
|
||||
:key="data.id || index"
|
||||
class="inv-card"
|
||||
>
|
||||
<view class="card-header">
|
||||
<text class="inv-title">{{ getTitle(data) }}</text>
|
||||
<text class="inv-status" :class="getStatusClass(data.spJd, data.spResult)">
|
||||
{{ getStatusText(data.spJd, data.spResult) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">申请人:</text>
|
||||
<text class="value">{{ data.applyUserName || "—" }}</text>
|
||||
<text class="label label-compact">申请日期:</text>
|
||||
<text class="value">{{ formatDate(data.applyDate) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">申请原因:</text>
|
||||
<text class="value ellipsis">{{ data.applyReason || "—" }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">总数量:</text>
|
||||
<text class="value qty">{{ data.totalQty || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="action-text" @click="goToDetail(data)">
|
||||
{{ activeTab === 'pending' ? '审批' : '查看' }}
|
||||
<uni-icons type="arrowright" size="14" color="#007aff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/Search.vue";
|
||||
import { invApplyFindUserTodosPageApi } from "@/api/base/invApplyApi";
|
||||
import { navigateTo } from "@/utils/uniapp";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const filterTabs = [
|
||||
{ key: "pending", label: "待办" },
|
||||
{ key: "approved", label: "已办" },
|
||||
{ key: "all", label: "全部" },
|
||||
];
|
||||
|
||||
// 本地存储的 key
|
||||
const STORAGE_KEY = 'invsp_search_conditions';
|
||||
|
||||
// 查询条件状态
|
||||
const activeTab = ref("pending");
|
||||
const searchKeyword = ref("");
|
||||
const dataList = ref<any[]>([]);
|
||||
const pagingRef = ref<any>(null);
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 保存查询条件到本地存储
|
||||
const saveSearchConditions = () => {
|
||||
try {
|
||||
const conditions = {
|
||||
activeTab: activeTab.value,
|
||||
searchKeyword: searchKeyword.value,
|
||||
};
|
||||
uni.setStorageSync(STORAGE_KEY, conditions);
|
||||
} catch (error) {
|
||||
console.error('保存查询条件失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储恢复查询条件
|
||||
const restoreSearchConditions = () => {
|
||||
try {
|
||||
const conditions = uni.getStorageSync(STORAGE_KEY);
|
||||
if (conditions) {
|
||||
activeTab.value = conditions.activeTab || "pending";
|
||||
searchKeyword.value = conditions.searchKeyword || "";
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复查询条件失败:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getCurrentTeacherId = () => {
|
||||
const jsData = userStore.getJs;
|
||||
return jsData?.id || null;
|
||||
};
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
searchKeyword.value = keyword;
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
const switchTab = (tabKey: string) => {
|
||||
activeTab.value = tabKey;
|
||||
saveSearchConditions(); // 保存查询条件
|
||||
pagingRef.value?.reload();
|
||||
};
|
||||
|
||||
const filterInvData = (list: any[]) => {
|
||||
let filtered = list;
|
||||
if (searchKeyword.value) {
|
||||
const kw = searchKeyword.value.toLowerCase();
|
||||
filtered = filtered.filter((item) => {
|
||||
const applyUserName = (item.applyUserName || '').toLowerCase();
|
||||
const applyReason = (item.applyReason || '').toLowerCase();
|
||||
return applyUserName.includes(kw) || applyReason.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;
|
||||
const params: any = {
|
||||
jsId,
|
||||
page: pageNo,
|
||||
rows: pageSize,
|
||||
};
|
||||
|
||||
if (activeTab.value === "all") {
|
||||
params.dbZt = "";
|
||||
params.spType = "";
|
||||
params.spJd = "";
|
||||
} else if (activeTab.value === "pending") {
|
||||
params.dbZt = "A";
|
||||
params.spType = "SP";
|
||||
params.spJd = "A";
|
||||
} else if (activeTab.value === "approved") {
|
||||
// 为了让驳回数据也出现在已办列表,这里不过滤 dbZt,后续用前端筛选
|
||||
params.dbZt = "";
|
||||
params.spType = "SP";
|
||||
params.spJd = "Z";
|
||||
} else {
|
||||
// 默认全部(不含抄送)
|
||||
params.dbZt = "";
|
||||
params.spType = "SP";
|
||||
params.spJd = "";
|
||||
}
|
||||
|
||||
// 如果有搜索关键词,传递申请原因参数
|
||||
if (searchKeyword.value) {
|
||||
params.applyReason = searchKeyword.value;
|
||||
}
|
||||
|
||||
response = await invApplyFindUserTodosPageApi(params);
|
||||
|
||||
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);
|
||||
// 针对待办/已办做前端筛选:
|
||||
// - 待办:去掉驳回(spResult === 'C')
|
||||
// - 已办:仅保留完结/已办/驳回(spResult === 'C' 或 dbZt === 'B' 或 spJd === 'Z' 或 spResult === 'B')
|
||||
if (activeTab.value === "pending") {
|
||||
rows = rows.filter(item => item.spResult !== "C");
|
||||
} else if (activeTab.value === "approved") {
|
||||
rows = rows.filter(item => item.spResult === "C" || item.dbZt === "B" || item.spJd === "Z" || item.spResult === "B");
|
||||
}
|
||||
|
||||
const filtered = filterInvData(rows);
|
||||
console.log("领用审批列表 filtered:", filtered);
|
||||
console.log('是否还有更多数据(根据数据量判断):', filtered.length === pageSize);
|
||||
|
||||
// z-paging 的 v-model 会自动管理 dataList
|
||||
// complete() 方法会根据 pageNo 自动判断是替换(第一页)还是追加(后续页)
|
||||
// 不传递 total 参数,让 z-paging 根据返回的数据量自动判断
|
||||
// 如果返回的数据量等于 pageSize,就认为还有更多数据
|
||||
pagingRef.value?.complete(filtered);
|
||||
} catch (error) {
|
||||
console.error("加载领用审批列表失败:", error);
|
||||
uni.showToast({ title: "加载失败", icon: "none" });
|
||||
// z-paging 会自动管理 dataList,只需要调用 complete 即可
|
||||
pagingRef.value?.complete([]);
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = (item: any) => {
|
||||
if (item.applyUserName && item.applyReason) {
|
||||
return `${item.applyUserName} - ${item.applyReason}`;
|
||||
}
|
||||
return item.applyUserName || item.applyReason || "领用申请";
|
||||
};
|
||||
|
||||
// 状态文案优先级:驳回 > 完结 > 审核中 > 待审核
|
||||
// 注意:暂存数据已在后端过滤,不会出现在审批列表中
|
||||
const getStatusText = (spJd?: string, spResult?: string) => {
|
||||
if (spResult === "C") return "驳回";
|
||||
if (spJd === "Z" || spResult === "B") return "完结";
|
||||
if (spJd === "A") return "审核中";
|
||||
if (spResult === "A") return "待审核";
|
||||
// 如果都不满足,默认返回待审核(暂存数据已被后端过滤)
|
||||
return "待审核";
|
||||
};
|
||||
|
||||
const getStatusClass = (spJd?: string, spResult?: string) => {
|
||||
if (spResult === "C") return "status-reject";
|
||||
if (spJd === "Z" || spResult === "B") return "status-done";
|
||||
if (spJd === "A" || spResult === "A") return "status-pending";
|
||||
// 如果都不满足,默认返回空(暂存数据已被后端过滤)
|
||||
return "";
|
||||
};
|
||||
|
||||
const formatDate = (val?: string) => (val ? dayjs(val).format("YYYY-MM-DD") : "--");
|
||||
|
||||
const goToDetail = (item: any) => {
|
||||
// 优先使用 xxtsId(如果有),否则使用业务 ID
|
||||
const id = item.xxtsId || item.ywId || item.id;
|
||||
if (!id) {
|
||||
uni.showToast({ title: "缺少申请ID", icon: "none" });
|
||||
return;
|
||||
}
|
||||
// 传递 dbZt 参数,用于判断是否已办
|
||||
const dbZt = item.dbZt || (activeTab.value === 'approved' ? 'B' : activeTab.value === 'pending' ? 'A' : '');
|
||||
navigateTo(`/pages/view/routine/inv/invsp/InvApplyApprove?id=${id}&from=db&dbZt=${dbZt}`);
|
||||
};
|
||||
|
||||
// 刷新列表的方法
|
||||
const refreshList = () => {
|
||||
if (pagingRef.value) {
|
||||
pagingRef.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听刷新事件
|
||||
const handleRefreshEvent = () => {
|
||||
refreshList();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 页面加载时恢复查询条件
|
||||
restoreSearchConditions();
|
||||
|
||||
// 监听刷新事件
|
||||
uni.$on('refreshInvApplyList', handleRefreshEvent);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 页面卸载时移除事件监听
|
||||
uni.$off('refreshInvApplyList', handleRefreshEvent);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
// 如果页面被重新初始化(如使用 redirectTo),恢复查询条件
|
||||
const hasRestored = restoreSearchConditions();
|
||||
// 恢复条件后需要重新加载数据
|
||||
if (hasRestored || pagingRef.value) {
|
||||
pagingRef.value?.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.invsp-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.top-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
background-color: #fff;
|
||||
padding: 10px 12px 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.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); // 减去顶部区域的高度
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden; // 重要:防止内容溢出
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.paging-container {
|
||||
height: 100%; // 使用固定高度,让 z-paging 能正确计算滚动区域
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.inv-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inv-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.inv-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff7e6;
|
||||
color: #f59e0b;
|
||||
}
|
||||
.status-done {
|
||||
background: #e8fff3;
|
||||
color: #10b981;
|
||||
}
|
||||
.status-reject {
|
||||
background: #ffecec;
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
width: 76px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label-compact {
|
||||
width: auto;
|
||||
min-width: 74px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.qty {
|
||||
color: #fa8c16;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
1179
src/pages/view/routine/inv/invsq/Apply.vue
Normal file
1179
src/pages/view/routine/inv/invsq/Apply.vue
Normal file
File diff suppressed because it is too large
Load Diff
572
src/pages/view/routine/inv/invsq/detail.vue
Normal file
572
src/pages/view/routine/inv/invsq/detail.vue
Normal file
@ -0,0 +1,572 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<uni-icons type="spinner-cycle" size="24" color="#fff" class="loading-icon"></uni-icons>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="detail-scroll">
|
||||
<view class="detail-container">
|
||||
<!-- 申请信息区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">申请信息</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请人</text>
|
||||
<text class="info-value">{{ detailData.applyUserName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请日期</text>
|
||||
<text class="info-value">{{ formatDate(detailData.applyDate) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请原因</text>
|
||||
<text class="info-value">{{ detailData.applyReason || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请状态</text>
|
||||
<text class="info-value status-text" :class="getStatusClass(detailData)">
|
||||
{{ getStatusText(detailData) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" v-if="detailData.resultTime">
|
||||
<text class="info-label">处理时间</text>
|
||||
<text class="info-value">{{ formatDate(detailData.resultTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请明细区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<view class="title-line"></view>
|
||||
<text class="title-text">申请明细</text>
|
||||
</view>
|
||||
|
||||
<view class="item-list" v-if="detailData.items && detailData.items.length > 0">
|
||||
<view
|
||||
v-for="(item, index) in detailData.items"
|
||||
:key="index"
|
||||
class="item-row"
|
||||
>
|
||||
<view class="item-header">
|
||||
<text class="item-index">第{{ index + 1 }}项</text>
|
||||
</view>
|
||||
|
||||
<view class="item-detail">
|
||||
<view class="info-item">
|
||||
<text class="info-label">物品</text>
|
||||
<text class="info-value">{{ item.itemName || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">单位</text>
|
||||
<text class="info-value">{{ item.unitName || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item">
|
||||
<text class="info-label">数量</text>
|
||||
<text class="info-value">{{ item.applyQty || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-tip">
|
||||
<text>暂无明细数据</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 固定在底部的按钮组 -->
|
||||
<view class="fixed-bottom" v-if="showActions">
|
||||
<view class="button-group">
|
||||
<!-- 暂存:只有删除按钮 -->
|
||||
<button
|
||||
v-if="currentStatus === 'DRAFT'"
|
||||
class="delete-btn"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
|
||||
<!-- 审批中:只有撤回按钮 -->
|
||||
<button
|
||||
v-if="currentStatus === 'A'"
|
||||
class="withdraw-btn"
|
||||
@click="handleWithdraw"
|
||||
>
|
||||
撤回申请
|
||||
</button>
|
||||
|
||||
<!-- 驳回:只有重新提交按钮 -->
|
||||
<button
|
||||
v-if="currentStatus === 'C'"
|
||||
class="resubmit-btn"
|
||||
@click="handleResubmit"
|
||||
>
|
||||
重新提交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { invApplyFindFullByIdApi, invApplyDeleteApi, invApplyWithdrawApi, invApplyReSubmitApi } from '@/api/base/invApplyApi';
|
||||
import { showToast } from "@/utils/uniapp";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// 响应式数据
|
||||
const detailData = ref<any>({});
|
||||
const loading = ref(false);
|
||||
const loadingText = ref('加载中...');
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string | Date) => {
|
||||
if (!dateStr) return '';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
// 判断状态:暂存、审批中、同意、驳回(仅基于 spJd 和 spResult,不使用 billStatus)
|
||||
const getStatus = (data: any) => {
|
||||
const spJd = data.spJd;
|
||||
const spResult = data.spResult;
|
||||
|
||||
// 驳回:spResult === 'C'
|
||||
if (spResult === 'C') {
|
||||
return 'C'; // 驳回
|
||||
}
|
||||
|
||||
// 同意/完结:spJd === 'Z' 或 spResult === 'B'
|
||||
if (spJd === 'Z' || spResult === 'B') {
|
||||
return 'B'; // 同意
|
||||
}
|
||||
|
||||
// 审批中:spJd === 'A' 或 spResult === 'A'
|
||||
if (spJd === 'A' || spResult === 'A') {
|
||||
return 'A'; // 审批中
|
||||
}
|
||||
|
||||
// 暂存:spJd 和 spResult 均为空(null 或空字符串)
|
||||
if ((!spJd || spJd === '') && (!spResult || spResult === '')) {
|
||||
return 'DRAFT'; // 暂存
|
||||
}
|
||||
|
||||
// 默认返回暂存
|
||||
return 'DRAFT';
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (data: any) => {
|
||||
const status = getStatus(data);
|
||||
switch (status) {
|
||||
case 'DRAFT': return '暂存';
|
||||
case 'A': return '审批中';
|
||||
case 'B': return '同意';
|
||||
case 'C': return '驳回';
|
||||
default: return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (data: any) => {
|
||||
const status = getStatus(data);
|
||||
switch (status) {
|
||||
case 'DRAFT': return 'status-draft';
|
||||
case 'A': return 'status-pending';
|
||||
case 'B': return 'status-approved';
|
||||
case 'C': return 'status-rejected';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 计算当前状态
|
||||
const currentStatus = computed(() => {
|
||||
return getStatus(detailData.value);
|
||||
});
|
||||
|
||||
// 判断是否显示按钮
|
||||
const showActions = computed(() => {
|
||||
const status = currentStatus.value;
|
||||
// 同意状态不显示按钮
|
||||
return status !== 'B';
|
||||
});
|
||||
|
||||
// 加载详情数据
|
||||
const loadDetailData = async (id: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
loadingText.value = '加载中...';
|
||||
|
||||
const res: any = await invApplyFindFullByIdApi({ id });
|
||||
|
||||
// 后端接口直接返回 InvApply 对象,也可能包装在 ReturnMsg 中
|
||||
let data = null;
|
||||
if (res && res.resultCode === 1 && res.result) {
|
||||
// 包装在 ReturnMsg 中的情况
|
||||
data = res.result;
|
||||
} else if (res && res.id) {
|
||||
// 直接返回对象的情况
|
||||
data = res;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
detailData.value = data;
|
||||
} else {
|
||||
showToast({
|
||||
title: '获取详情失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载详情失败:', error);
|
||||
showToast({
|
||||
title: '加载详情失败: ' + (error?.message || '未知错误'),
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除申请
|
||||
const handleDelete = async () => {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个申请吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const deleteRes: any = await invApplyDeleteApi({ ids: detailData.value.id });
|
||||
if (deleteRes && deleteRes.resultCode === 1) {
|
||||
showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 800);
|
||||
} else {
|
||||
showToast({
|
||||
title: deleteRes?.message || '删除失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 撤回申请
|
||||
const handleWithdraw = async () => {
|
||||
uni.showModal({
|
||||
title: '确认撤回',
|
||||
content: '确定要撤回该申请吗?撤回后将无法恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '撤回中...' });
|
||||
const result: any = await invApplyWithdrawApi({
|
||||
id: detailData.value.id,
|
||||
spRemark: '申请人撤回'
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
// 判断成功:resultCode === 1 或 resultCode === 0
|
||||
if (result && (result.resultCode === 1 || result.resultCode === 0)) {
|
||||
showToast({
|
||||
title: result.message || '撤回成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
// 延迟返回,确保提示信息显示
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
}, 500);
|
||||
} else {
|
||||
console.warn('撤回失败,返回结果:', result);
|
||||
showToast({
|
||||
title: result?.message || result?.msg || '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('撤回失败,异常信息:', error);
|
||||
showToast({
|
||||
title: '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 重新提交
|
||||
const handleResubmit = () => {
|
||||
if (!detailData.value.id) {
|
||||
showToast({
|
||||
title: '缺少申请ID',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 跳转到申请页面,传递ID和重新提交标识
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/inv/invsq/Apply?id=${detailData.value.id}&resubmit=true`
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载
|
||||
onLoad(async (options: any) => {
|
||||
if (options && options.id) {
|
||||
await loadDetailData(options.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-scroll {
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
padding: 20rpx;
|
||||
padding-bottom: 160rpx; // 为底部按钮留出空间
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
|
||||
&.status-text {
|
||||
&.status-draft {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
&.status-approved {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.status-rejected {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&.total-value {
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
.item-row {
|
||||
border: 1rpx solid #e4e7ed;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fafafa;
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #e4e7ed;
|
||||
|
||||
.item-index {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
.info-item {
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx dashed #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 固定在底部的按钮组 */
|
||||
.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: 1;
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.edit-btn {
|
||||
background: #9ca3af;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.delete-btn {
|
||||
background: #f87171;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.withdraw-btn {
|
||||
background: linear-gradient(135deg, #ff9800 0%, #ffa726 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 152, 0, 0.35);
|
||||
}
|
||||
|
||||
&.resubmit-btn {
|
||||
background: linear-gradient(135deg, #f5222d 0%, #ff4d4f 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(245, 34, 45, 0.35);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载遮罩层样式
|
||||
.loading-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
min-width: 200rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
549
src/pages/view/routine/inv/invsq/index.vue
Normal file
549
src/pages/view/routine/inv/invsq/index.vue
Normal file
@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<view class="container">
|
||||
<!-- 顶部状态筛选(固定) -->
|
||||
<view class="query-section">
|
||||
<view class="search-card">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<picker mode="selector" :range="statusOptions" range-key="text" :value="selectedStatusIndex" @change="onStatusChange">
|
||||
<view class="picker-text">{{ selectedStatusText || '全部状态' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-actions">
|
||||
<u-button text="查询" type="primary" size="small" @click="handleSearch" />
|
||||
<u-button text="重置" type="info" size="small" @click="handleReset" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容滚动区 -->
|
||||
<scroll-view scroll-y class="content-scroll">
|
||||
<uni-card :is-shadow="false" is-full>
|
||||
<view class="header">
|
||||
<text class="total-text">申请总数: {{ applyList.length }}</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :is-shadow="false" is-full margin="10px 0 0 0">
|
||||
<view class="list-header">
|
||||
<text class="applicant-header">申请人</text>
|
||||
<text class="date-header">日期</text>
|
||||
<text class="qty-header">数量</text>
|
||||
<text class="status-header">状态</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="list-item"
|
||||
v-for="item in applyList"
|
||||
:key="item.id"
|
||||
@click="handleViewDetail(item)"
|
||||
>
|
||||
<view class="row-cols">
|
||||
<text class="applicant-col">{{ item.applyUserName || '-' }}</text>
|
||||
<text class="date-col">{{ formatDate(item.applyDate) || '-' }}</text>
|
||||
<text class="qty-col">{{ item.totalQty ?? 0 }}</text>
|
||||
<view class="status-col">
|
||||
<text class="status-badge" :class="getStatusClass(item)">{{ getStatusText(item) }}</text>
|
||||
<u-icon name="arrow-right" size="16" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!loading && applyList.length === 0" class="empty-container">
|
||||
<text class="empty-text">暂无申请记录</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<template #bottom>
|
||||
<view class="white-bg-color py-5">
|
||||
<view class="flex-row items-center pb-10 pt-5">
|
||||
<u-button
|
||||
text="新增申请"
|
||||
class="ml-15 mr-15"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { invApplyFindPageApi, invApplyFindAllApi } from "@/api/base/invApplyApi";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { getJs } = useUserStore();
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: null, text: '全部状态' },
|
||||
{ value: 'DRAFT', text: '暂存' },
|
||||
{ value: 'A', text: '审批中' },
|
||||
{ value: 'B', text: '同意' },
|
||||
{ value: 'C', text: '驳回' }
|
||||
];
|
||||
|
||||
// 响应式数据
|
||||
const applyList = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadingStatus = ref('more'); // more/loading/noMore
|
||||
const pageParams = reactive({
|
||||
page: 1,
|
||||
rows: 10,
|
||||
billStatus: null,
|
||||
spResult: null
|
||||
});
|
||||
|
||||
const selectedStatus = ref<string | null>(null);
|
||||
const selectedStatusText = ref('全部状态');
|
||||
const selectedStatusIndex = ref(0);
|
||||
|
||||
// 获取申请列表
|
||||
const getApplyList = async (reset = false) => {
|
||||
if (reset) {
|
||||
pageParams.page = 1;
|
||||
loadingStatus.value = 'more';
|
||||
}
|
||||
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
...pageParams,
|
||||
applyUserId: getJs?.id // 只查询当前用户的数据
|
||||
};
|
||||
|
||||
const res: any = await invApplyFindPageApi(params);
|
||||
const list = res?.result?.rows || res?.rows || [];
|
||||
|
||||
if (reset) {
|
||||
applyList.value = list;
|
||||
} else {
|
||||
applyList.value = [...applyList.value, ...list];
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (list.length < pageParams.rows) {
|
||||
loadingStatus.value = 'noMore';
|
||||
} else {
|
||||
loadingStatus.value = 'more';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取申请列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 状态选择变化
|
||||
const onStatusChange = (e: any) => {
|
||||
const index = e.detail.value;
|
||||
selectedStatusIndex.value = index;
|
||||
const selectedOption = statusOptions[index];
|
||||
selectedStatus.value = selectedOption.value;
|
||||
selectedStatusText.value = selectedOption.text;
|
||||
};
|
||||
|
||||
// 查询
|
||||
const handleSearch = () => {
|
||||
const status = selectedStatus.value;
|
||||
// 如果是特殊状态值,需要转换
|
||||
if (status === 'DRAFT') {
|
||||
pageParams.billStatus = 0;
|
||||
pageParams.spResult = null;
|
||||
} else {
|
||||
pageParams.billStatus = null;
|
||||
pageParams.spResult = status;
|
||||
}
|
||||
pageParams.page = 1;
|
||||
getApplyList(true);
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
selectedStatusIndex.value = 0;
|
||||
selectedStatus.value = null;
|
||||
selectedStatusText.value = '全部状态';
|
||||
pageParams.spResult = null;
|
||||
pageParams.billStatus = null;
|
||||
pageParams.page = 1;
|
||||
getApplyList(true);
|
||||
};
|
||||
|
||||
// 判断状态:暂存、审批中、同意、驳回(仅基于 spJd 和 spResult,不使用 billStatus)
|
||||
const getStatus = (item: any) => {
|
||||
const spJd = item.spJd;
|
||||
const spResult = item.spResult;
|
||||
|
||||
// 驳回:spResult === 'C'
|
||||
if (spResult === 'C') {
|
||||
return 'C'; // 驳回
|
||||
}
|
||||
|
||||
// 同意/完结:spJd === 'Z' 或 spResult === 'B'
|
||||
if (spJd === 'Z' || spResult === 'B') {
|
||||
return 'B'; // 同意
|
||||
}
|
||||
|
||||
// 审批中:spJd === 'A' 或 spResult === 'A'
|
||||
if (spJd === 'A' || spResult === 'A') {
|
||||
return 'A'; // 审批中
|
||||
}
|
||||
|
||||
// 暂存:spJd 和 spResult 均为空(null 或空字符串)
|
||||
if ((!spJd || spJd === '') && (!spResult || spResult === '')) {
|
||||
return 'DRAFT'; // 暂存
|
||||
}
|
||||
|
||||
// 默认返回暂存
|
||||
return 'DRAFT';
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (item: any) => {
|
||||
const status = getStatus(item);
|
||||
switch (status) {
|
||||
case 'DRAFT': return '暂';
|
||||
case 'A': return '审';
|
||||
case 'B': return '同';
|
||||
case 'C': return '驳';
|
||||
default: return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (item: any) => {
|
||||
const status = getStatus(item);
|
||||
switch (status) {
|
||||
case 'DRAFT': return 'status-draft';
|
||||
case 'A': return 'status-pending';
|
||||
case 'B': return 'status-approved';
|
||||
case 'C': return 'status-rejected';
|
||||
default: return 'status-default';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
// 查看详情或编辑
|
||||
const handleViewDetail = (item: any) => {
|
||||
const status = getStatus(item);
|
||||
|
||||
// 暂存和驳回状态,跳转到申请页面进行编辑
|
||||
if (status === 'DRAFT' || status === 'C') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/inv/invsq/Apply?id=${item.id}${status === 'C' ? '&resubmit=true' : ''}`
|
||||
});
|
||||
} else {
|
||||
// 其他状态,跳转到详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/inv/invsq/detail?id=${item.id}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 新增申请
|
||||
const handleAdd = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/inv/invsq/Apply'
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
getApplyList(true);
|
||||
});
|
||||
|
||||
// 页面显示时刷新列表(从详情页返回时)
|
||||
onShow(() => {
|
||||
getApplyList(true);
|
||||
});
|
||||
|
||||
// 上拉加载(uni-app 页面生命周期)
|
||||
const onReachBottom = () => {
|
||||
if (loadingStatus.value !== 'more') return;
|
||||
|
||||
pageParams.page++;
|
||||
getApplyList();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部查询区域(固定) */
|
||||
.query-section {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
padding-bottom: 10rpx;
|
||||
background: linear-gradient(180deg, #f8f8f8 70%, rgba(248, 248, 248, 0));
|
||||
}
|
||||
|
||||
.search-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16rpx 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 时间筛选样式 */
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 4rpx 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
background-color: #f8f9fa;
|
||||
border: 1rpx solid #e9ecef;
|
||||
border-radius: 8rpx;
|
||||
padding: 14rpx 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 240rpx;
|
||||
|
||||
:deep(.u-button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: calc(100vh - 240rpx); // 留出顶部筛选和底部按钮空间
|
||||
padding: 10rpx 0 50rpx;
|
||||
}
|
||||
|
||||
// Remove default card padding if needed
|
||||
::v-deep .uni-card .uni-card__content {
|
||||
padding: 10px 15px !important; // Overwrite default padding
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.applicant-header {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.date-header {
|
||||
flex: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qty-header {
|
||||
flex: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-cols {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.applicant-col {
|
||||
flex: 1.2;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-col {
|
||||
flex: 1.2;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qty-col {
|
||||
flex: 0.8;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
margin-right: 8px;
|
||||
|
||||
&.status-draft {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: #fef3c7;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
&.status-approved {
|
||||
background: #d1fae5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.status-rejected {
|
||||
background: #fee2e2;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
&.status-default {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.white-bg-color {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.pt-5 {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.ml-15 {
|
||||
margin-left: 60rpx;
|
||||
}
|
||||
|
||||
.mr-15 {
|
||||
margin-right: 60rpx;
|
||||
}
|
||||
</style>
|
||||
@ -139,7 +139,8 @@ const loadQuestionnaireList = async () => {
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
rows: 100
|
||||
rows: 100,
|
||||
quStatus: 'A' // 只查询已发布的问卷
|
||||
};
|
||||
|
||||
// 添加时间范围查询
|
||||
|
||||
@ -115,7 +115,8 @@
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { kccyFindByKcjbIdApi } from "@/api/base/kccyApi";
|
||||
import { rwPushJsApi, rwGetPushedJsIdsApi } from "@/api/base/rwApi";
|
||||
import { rwPushJsApi } from "@/api/base/rwApi";
|
||||
import { executedInfoByRwIdApi } from "@/api/base/rwzxApi";
|
||||
|
||||
// 接口类型定义
|
||||
interface TaskInfo {
|
||||
@ -239,14 +240,14 @@ const loadCourseMembers = async () => {
|
||||
try {
|
||||
console.log('开始加载课程成员,课程ID:', courseId.value);
|
||||
|
||||
// 并发请求:获取成员列表和已推送教师ID
|
||||
const [memberResponse, pushedResponse] = await Promise.all([
|
||||
// 并发请求:获取成员列表和任务执行记录(用于判断是否已推送)
|
||||
const [memberResponse, executionResponse] = await Promise.all([
|
||||
kccyFindByKcjbIdApi({ kcjbId: courseId.value }),
|
||||
rwGetPushedJsIdsApi({ rwId: taskInfo.value.id })
|
||||
executedInfoByRwIdApi({ rwId: taskInfo.value.id })
|
||||
]);
|
||||
|
||||
console.log('课程成员API响应:', memberResponse);
|
||||
console.log('已推送教师API响应:', pushedResponse);
|
||||
console.log('任务执行记录API响应:', executionResponse);
|
||||
|
||||
// 处理成员数据
|
||||
let memberData = [];
|
||||
@ -266,14 +267,28 @@ const loadCourseMembers = async () => {
|
||||
|
||||
memberList.value = memberData;
|
||||
|
||||
// 处理已推送教师ID
|
||||
if (pushedResponse && pushedResponse.resultCode === 1) {
|
||||
pushedJsIds.value = pushedResponse.result || [];
|
||||
console.log('已推送的教师ID:', pushedJsIds.value);
|
||||
} else {
|
||||
pushedJsIds.value = [];
|
||||
// 处理任务执行记录,提取已推送的教师ID(iszx = 1)
|
||||
let executionData = [];
|
||||
if (executionResponse) {
|
||||
if (executionResponse.hasOwnProperty('resultCode')) {
|
||||
if (executionResponse.resultCode === 1 || executionResponse.resultCode === 0) {
|
||||
executionData = executionResponse.result || executionResponse.rows || executionResponse.data || [];
|
||||
}
|
||||
} else if (executionResponse.rows || executionResponse.data) {
|
||||
executionData = executionResponse.rows || executionResponse.data || [];
|
||||
} else if (Array.isArray(executionResponse)) {
|
||||
executionData = executionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// 从任务执行记录中提取已推送的教师ID(iszx = 1)
|
||||
pushedJsIds.value = executionData
|
||||
.filter((item: any) => item.iszx === 1) // 筛选 iszx = 1(已推送)的记录
|
||||
.map((item: any) => item.rwzxfzr) // 提取教师ID
|
||||
.filter((jsId: string) => jsId != null && jsId.trim() !== ''); // 过滤空值
|
||||
|
||||
console.log('已推送的教师ID(基于iszx字段):', pushedJsIds.value);
|
||||
|
||||
// 默认勾选未推送的成员,不勾选已推送的成员
|
||||
memberList.value.forEach(member => {
|
||||
const isPushed = pushedJsIds.value.includes(member.jsId);
|
||||
|
||||
@ -140,7 +140,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onUnmounted } from "vue";
|
||||
import { onLoad as onPageLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { rwFindPageSummaryApi } from "@/api/base/rwApi";
|
||||
import { kccyFindByKcjbIdApi } from "@/api/base/kccyApi";
|
||||
import { jycyFindByJyjbIdApi } from "@/api/base/jycyApi";
|
||||
import { getTeacherTasksApi } from "@/api/base/rwzxApi";
|
||||
|
||||
BIN
src/static/base/home/invly.png
Normal file
BIN
src/static/base/home/invly.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/static/base/home/invout.png
Normal file
BIN
src/static/base/home/invout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
BIN
src/static/base/home/invsp.png
Normal file
BIN
src/static/base/home/invsp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/static/base/home/jctjqd.png
Normal file
BIN
src/static/base/home/jctjqd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
54
src/types/jweixin-js-sdk.d.ts
vendored
Normal file
54
src/types/jweixin-js-sdk.d.ts
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 微信 JS-SDK 类型声明
|
||||
* 为 jweixin-js-sdk npm 包提供 TypeScript 类型支持
|
||||
*/
|
||||
|
||||
declare module 'jweixin-js-sdk' {
|
||||
interface ConfigOptions {
|
||||
debug?: boolean;
|
||||
appId: string;
|
||||
timestamp: number;
|
||||
nonceStr: string;
|
||||
signature: string;
|
||||
jsApiList: string[];
|
||||
}
|
||||
|
||||
interface ShareData {
|
||||
title: string;
|
||||
desc?: string;
|
||||
link: string;
|
||||
imgUrl?: string;
|
||||
success?: () => void;
|
||||
cancel?: () => void;
|
||||
fail?: (res: any) => void;
|
||||
complete?: (res: any) => void;
|
||||
}
|
||||
|
||||
interface ErrorRes {
|
||||
errMsg: string;
|
||||
}
|
||||
|
||||
interface WxJSSDK {
|
||||
config(options: ConfigOptions): void;
|
||||
ready(callback: () => void): void;
|
||||
error(callback: (res: ErrorRes) => void): void;
|
||||
updateAppMessageShareData(data: ShareData): void;
|
||||
updateTimelineShareData(data: ShareData): void;
|
||||
checkJsApi(params: {
|
||||
jsApiList: string[];
|
||||
success?: (res: { checkResult: Record<string, boolean> }) => void;
|
||||
fail?: (res: ErrorRes) => void;
|
||||
complete?: (res: any) => void;
|
||||
}): void;
|
||||
}
|
||||
|
||||
const wx: WxJSSDK;
|
||||
export default wx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -124,7 +124,8 @@ export function get<T = any>(
|
||||
reject(res);
|
||||
}
|
||||
} else {
|
||||
if (res.rows) {
|
||||
// 检查是否是直接返回的对象(有 id 字段,说明是实体对象)
|
||||
if (res.rows || res.id) {
|
||||
resolve(res);
|
||||
} else {
|
||||
showToast(res.message || "接口异常");
|
||||
|
||||
313
src/utils/wechatShare.ts
Normal file
313
src/utils/wechatShare.ts
Normal file
@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 微信分享工具类
|
||||
* 用于在微信浏览器内实现分享功能
|
||||
*/
|
||||
import { wxConfigApi } from '@/api/system/login';
|
||||
// #ifdef H5
|
||||
// jweixin-js-sdk 是 UMD 格式,使用动态导入
|
||||
let wx: any;
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 检测是否在微信浏览器环境
|
||||
*/
|
||||
export function isWeixinBrowser(): boolean {
|
||||
// #ifdef H5
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return false;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面完整URL(去掉hash部分)
|
||||
* 微信签名需要使用不含hash的URL
|
||||
*/
|
||||
export function getCurrentUrl(): string {
|
||||
// #ifdef H5
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 获取基础URL(协议 + 域名 + 路径)
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
const pathname = window.location.pathname;
|
||||
const search = window.location.search;
|
||||
|
||||
// 组合完整URL(不含hash)
|
||||
return `${protocol}//${host}${pathname}${search}`;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return '';
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信分享配置接口返回的数据结构
|
||||
*/
|
||||
interface WxConfigResult {
|
||||
appId: string;
|
||||
timestamp: number;
|
||||
nonceStr: string;
|
||||
signature: string;
|
||||
jsapi_ticket?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享内容配置
|
||||
*/
|
||||
export interface ShareConfig {
|
||||
title: string;
|
||||
desc?: string;
|
||||
link: string;
|
||||
imgUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化微信分享
|
||||
* @param shareConfig 分享内容配置
|
||||
* @param debug 是否开启调试模式
|
||||
*/
|
||||
export async function initWechatShare(
|
||||
shareConfig: ShareConfig,
|
||||
debug: boolean = false
|
||||
): Promise<void> {
|
||||
// #ifdef H5
|
||||
// 检查是否在微信浏览器环境
|
||||
if (!isWeixinBrowser()) {
|
||||
console.warn('当前不在微信浏览器环境,无法使用微信分享功能');
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态加载微信JS-SDK
|
||||
try {
|
||||
// 先检查全局变量是否存在
|
||||
if (typeof window !== 'undefined') {
|
||||
wx = (window as any).wx || (window as any).jWeixin;
|
||||
|
||||
// 如果全局变量存在且有效,直接使用
|
||||
if (wx && typeof wx.config === 'function') {
|
||||
// 全局变量已存在,直接使用
|
||||
}
|
||||
}
|
||||
|
||||
// 如果全局变量不存在或无效,使用 script 标签动态加载(使用 CDN)
|
||||
if (!wx || typeof wx.config !== 'function') {
|
||||
// 检查是否已经加载过 script
|
||||
const existingScript = document.querySelector('script[data-jweixin]');
|
||||
if (existingScript) {
|
||||
// 等待一下,然后从全局获取
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
wx = (window as any).wx || (window as any).jWeixin;
|
||||
} else {
|
||||
// 动态创建 script 标签从 CDN 加载
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.setAttribute('data-jweixin', 'true');
|
||||
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||
|
||||
script.onload = () => {
|
||||
// 等待一下,确保 UMD 模块执行完成并设置全局变量
|
||||
setTimeout(() => {
|
||||
wx = (window as any).wx || (window as any).jWeixin;
|
||||
|
||||
if (wx && typeof wx.config === 'function') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('微信JS-SDK加载失败:wx对象无效'));
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error('微信JS-SDK脚本加载失败'));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载微信JS-SDK失败:', error);
|
||||
uni.showToast({
|
||||
title: '分享功能初始化失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查微信JS-SDK是否加载成功
|
||||
if (typeof wx === 'undefined' || !wx) {
|
||||
console.error('微信JS-SDK未加载,请检查npm包是否正确安装');
|
||||
uni.showToast({
|
||||
title: '分享功能初始化失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 wx 对象是否有效
|
||||
if (typeof wx.config !== 'function') {
|
||||
console.error('❌ wx.config 不是函数!');
|
||||
console.error('wx 对象:', wx);
|
||||
console.error('wx 对象的属性:', Object.keys(wx || {}));
|
||||
uni.showToast({
|
||||
title: '微信JS-SDK配置失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前页面URL(去掉hash)
|
||||
const currentUrl = getCurrentUrl();
|
||||
if (!currentUrl) {
|
||||
console.error('无法获取当前页面URL');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('获取微信签名配置,URL:', currentUrl);
|
||||
|
||||
// 调用后端接口获取签名配置
|
||||
const response = await wxConfigApi({ url: currentUrl });
|
||||
|
||||
if (!response || response.resultCode !== 1 || !response.result) {
|
||||
console.error('获取微信签名配置失败:', response);
|
||||
uni.showToast({
|
||||
title: '分享配置获取失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const configData: WxConfigResult = response.result;
|
||||
|
||||
// 配置微信JS-SDK
|
||||
wx.config({
|
||||
debug: debug, // 开启调试模式,在微信内打开调试信息
|
||||
appId: configData.appId,
|
||||
timestamp: configData.timestamp,
|
||||
nonceStr: configData.nonceStr,
|
||||
signature: configData.signature,
|
||||
jsApiList: [
|
||||
'updateAppMessageShareData', // 分享给朋友
|
||||
'updateTimelineShareData' // 分享到朋友圈
|
||||
]
|
||||
});
|
||||
|
||||
// 配置成功后的回调
|
||||
wx.ready(() => {
|
||||
// 确保图片URL是完整的网络URL
|
||||
let finalImgUrl = shareConfig.imgUrl || '';
|
||||
if (finalImgUrl && !finalImgUrl.startsWith('http://') && !finalImgUrl.startsWith('https://')) {
|
||||
// 如果不是完整URL,尝试转换为完整URL
|
||||
if (typeof window !== 'undefined') {
|
||||
const baseUrl = window.location.protocol + '//' + window.location.host;
|
||||
finalImgUrl = finalImgUrl.startsWith('/') ? baseUrl + finalImgUrl : baseUrl + '/' + finalImgUrl;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('设置分享内容 - 标题:', shareConfig.title);
|
||||
console.log('设置分享内容 - 链接:', shareConfig.link);
|
||||
console.log('设置分享内容 - 封面图URL:', finalImgUrl);
|
||||
|
||||
// 设置分享给朋友的内容
|
||||
wx.updateAppMessageShareData({
|
||||
title: shareConfig.title,
|
||||
desc: shareConfig.desc || shareConfig.title,
|
||||
link: shareConfig.link,
|
||||
imgUrl: finalImgUrl,
|
||||
success: () => {
|
||||
console.log('✅ 分享给朋友配置成功');
|
||||
},
|
||||
cancel: () => {
|
||||
// 用户取消分享
|
||||
},
|
||||
fail: (res: any) => {
|
||||
console.error('❌ 分享给朋友配置失败:', res);
|
||||
}
|
||||
});
|
||||
|
||||
// 设置分享到朋友圈的内容
|
||||
wx.updateTimelineShareData({
|
||||
title: shareConfig.title,
|
||||
link: shareConfig.link,
|
||||
imgUrl: finalImgUrl,
|
||||
success: () => {
|
||||
console.log('✅ 分享到朋友圈配置成功');
|
||||
},
|
||||
cancel: () => {
|
||||
// 用户取消分享
|
||||
},
|
||||
fail: (res: any) => {
|
||||
console.error('❌ 分享到朋友圈配置失败:', res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 配置失败的回调
|
||||
wx.error((res: any) => {
|
||||
console.error('微信JS-SDK配置失败:', res);
|
||||
uni.showToast({
|
||||
title: '分享功能配置失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化微信分享失败:', error);
|
||||
uni.showToast({
|
||||
title: '分享功能初始化失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
console.warn('非H5环境,不支持微信分享功能');
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分享链接
|
||||
* @param baseUrl 基础URL
|
||||
* @param params 参数对象
|
||||
*/
|
||||
export function buildShareLink(baseUrl: string, params: Record<string, string>): string {
|
||||
// #ifdef H5
|
||||
try {
|
||||
// 尝试使用 URL 构造函数(现代浏览器)
|
||||
if (typeof URL !== 'undefined') {
|
||||
const url = new URL(baseUrl);
|
||||
Object.keys(params).forEach(key => {
|
||||
url.searchParams.set(key, params[key]);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
// URL 构造函数不可用,使用字符串拼接方式
|
||||
}
|
||||
|
||||
// 降级方案:使用字符串拼接
|
||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
|
||||
return `${baseUrl}${separator}${queryString}`;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return baseUrl;
|
||||
// #endif
|
||||
}
|
||||
|
||||
@ -16,19 +16,6 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
// 确保文件名包含hash(Vite默认已包含,这里显式配置)
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 确保文件名包含hash,文件名变化就代表新版本
|
||||
entryFileNames: `assets/[name]-[hash].js`,
|
||||
chunkFileNames: `assets/[name]-[hash].js`,
|
||||
assetFileNames: `assets/[name]-[hash].[ext]`
|
||||
}
|
||||
},
|
||||
// 生成manifest文件,用于版本管理(可选)
|
||||
manifest: true
|
||||
},
|
||||
plugins: [
|
||||
//c 为class 例如 class="wi-10"
|
||||
//w 为样式style 例如 width:10rpx
|
||||
@ -71,5 +58,25 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, './src'),
|
||||
'#': resolve(__dirname, './types')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['jweixin-js-sdk']
|
||||
},
|
||||
build: {
|
||||
// 确保文件名包含hash(Vite默认已包含,这里显式配置)
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 确保文件名包含hash,文件名变化就代表新版本
|
||||
entryFileNames: `assets/[name]-[hash].js`,
|
||||
chunkFileNames: `assets/[name]-[hash].js`,
|
||||
assetFileNames: `assets/[name]-[hash].[ext]`
|
||||
}
|
||||
},
|
||||
// 生成manifest文件,用于版本管理(可选)
|
||||
manifest: true,
|
||||
commonjsOptions: {
|
||||
include: [/jweixin-js-sdk/, /node_modules/],
|
||||
transformMixedEsModules: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user