临时易耗品申请与审批
This commit is contained in:
parent
7734c0cd89
commit
b0e010f80e
37
src/App.vue
37
src/App.vue
@ -1,17 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app"
|
||||
import { startPolling, stopPolling } from "@/utils/messageManager"
|
||||
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
});
|
||||
console.log("App Launch")
|
||||
// 启动消息轮询(30秒一次)
|
||||
startPolling(30000)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
console.log("App Show");
|
||||
});
|
||||
console.log("App Show")
|
||||
// 应用从后台切换到前台时,重新启动轮询
|
||||
startPolling(30000)
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
console.log("App Hide")
|
||||
// 应用切换到后台时,停止轮询以节省资源
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
body{
|
||||
/* 全局盒模型设置 */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 全局字体设置 */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* uni-app 组件也应用 border-box */
|
||||
page, view, text, image, button, input, textarea, scroll-view, swiper, picker {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -110,3 +110,9 @@ export default {
|
||||
unloadSpecimentPic: "/API/specimentransport/finishpic/upload/",
|
||||
},
|
||||
};
|
||||
|
||||
export const enum AssetRole {
|
||||
DEVICE_MANAGER = 0b100,
|
||||
TEAM_LEAD = 0b010,
|
||||
CONSUMABLES_MANAGER = 0b001
|
||||
}
|
||||
@ -118,8 +118,43 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "易耗品盘点(进行中)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-request",
|
||||
"style": {
|
||||
"navigationBarTitleText": "临时易耗品申请"
|
||||
}
|
||||
,
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-request-create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新建申请"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-request-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "申请详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-approve",
|
||||
"style": {
|
||||
"navigationBarTitleText": "临时易耗品审批"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-approve-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "审批详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/temp-catalog",
|
||||
"style": {
|
||||
"navigationBarTitleText": "耗材种类管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/scan",
|
||||
"style": {
|
||||
|
||||
142
src/pages/api/assets.ts
Normal file
142
src/pages/api/assets.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { formatTime, formatDate, formatDateTime, BASE_URL, getHeaders } from './index.js';
|
||||
|
||||
// 定义借用资产接口
|
||||
interface BorrowAsset {
|
||||
borrowId: number;
|
||||
assetId: number;
|
||||
assetName: string;
|
||||
borrowDate: string;
|
||||
returnDate: string | null;
|
||||
actualReturnDate: string | null;
|
||||
borrowerId: string;
|
||||
borrowerName: string;
|
||||
borrowerMobile: string;
|
||||
borrowDepartmentId: string | null;
|
||||
borrowDepartmentName: string | null;
|
||||
location: string | null;
|
||||
lenderId: string | null;
|
||||
lenderName: string | null;
|
||||
lenderMobile: string | null;
|
||||
approverId: string | null;
|
||||
approverName: string | null;
|
||||
approverMobile: string | null;
|
||||
registrantId: string | null;
|
||||
registrantName: string | null;
|
||||
registrantMobile: string | null;
|
||||
registrantDate: string | null;
|
||||
isReturn: number;
|
||||
note: string;
|
||||
deleteFlag: number;
|
||||
}
|
||||
|
||||
// 定义分页响应接口
|
||||
interface PagedResponse<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
size: number;
|
||||
startRow: number;
|
||||
endRow: number;
|
||||
pages: number;
|
||||
prePage: number;
|
||||
nextPage: number;
|
||||
isFirstPage: boolean;
|
||||
isLastPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
navigatePages: number;
|
||||
navigatepageNums: number[];
|
||||
navigateFirstPage: number;
|
||||
navigateLastPage: number;
|
||||
}
|
||||
|
||||
// 定义 API 响应接口
|
||||
interface ApiResponse<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
token: string | null;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 借用列表
|
||||
/* {
|
||||
"code": 0,
|
||||
"msg": "查询成功",
|
||||
"token": null,
|
||||
"data": {
|
||||
"total": 16,
|
||||
"list": [
|
||||
{
|
||||
"borrowId": 49,
|
||||
"assetId": 83,
|
||||
"assetName": "和法国哈哈",
|
||||
"borrowDate": "2025-09-07",
|
||||
"returnDate": "2025-09-01",
|
||||
"actualReturnDate": null,
|
||||
"borrowerId": "40288083894a5a38018999ea74ff0135",
|
||||
"borrowerName": "熊三",
|
||||
"borrowerMobile": "123",
|
||||
"borrowDepartmentId": null,
|
||||
"borrowDepartmentName": null,
|
||||
"location": null,
|
||||
"lenderId": null,
|
||||
"lenderName": null,
|
||||
"lenderMobile": null,
|
||||
"approverId": null,
|
||||
"approverName": null,
|
||||
"approverMobile": null,
|
||||
"registrantId": "null",
|
||||
"registrantName": null,
|
||||
"registrantMobile": null,
|
||||
"registrantDate": "2025-09-29",
|
||||
"isReturn": 0,
|
||||
"note": "123213",
|
||||
"deleteFlag": 0
|
||||
},...
|
||||
],
|
||||
"pageNum": 1,
|
||||
"pageSize": 10,
|
||||
"size": 10,
|
||||
"startRow": 1,
|
||||
"endRow": 10,
|
||||
"pages": 2,
|
||||
"prePage": 0,
|
||||
"nextPage": 2,
|
||||
"isFirstPage": true,
|
||||
"isLastPage": false,
|
||||
"hasPreviousPage": false,
|
||||
"hasNextPage": true,
|
||||
"navigatePages": 8,
|
||||
"navigatepageNums": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"navigateFirstPage": 1,
|
||||
"navigateLastPage": 2
|
||||
}
|
||||
} */
|
||||
export function getLentAssetsList(page: number, pageSize: number, searchParams: any): Promise<ApiResponse<PagedResponse<BorrowAsset>>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/borrow/page`,
|
||||
method: 'POST',
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
pageNum: page,
|
||||
pageSize,
|
||||
...searchParams
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data as ApiResponse<PagedResponse<BorrowAsset>>);
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
2831
src/pages/api/consumable-temp.oneapi.json
Normal file
2831
src/pages/api/consumable-temp.oneapi.json
Normal file
File diff suppressed because it is too large
Load Diff
188
src/pages/api/consumable-temp.ts
Normal file
188
src/pages/api/consumable-temp.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { formatTime, formatDate, formatDateTime, getHeaders } from './index';
|
||||
|
||||
const BASE_URL = 'http://localhost:3000' // 替换为你的实际后端地址
|
||||
// 请求封装
|
||||
const request = (url: string, options: any = {}) => {
|
||||
const token = uni.getStorageSync('token')
|
||||
return uni.request({
|
||||
url: `${BASE_URL}${url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
...options.header
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
export interface RequestItem {
|
||||
id?: string
|
||||
name: string
|
||||
spec?: string
|
||||
quantity: number
|
||||
unit?: string
|
||||
estimatedUnitCost?: number
|
||||
remark?: string
|
||||
catalogId?: string
|
||||
requestId?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface ConsumableRequest {
|
||||
id?: string
|
||||
applicantName?: string
|
||||
applicantPhone: string
|
||||
department?: string
|
||||
reason?: string
|
||||
status?: 'PENDING' | 'APPROVED' | 'REJECTED' | 'CANCELLED' | 'COMPLETED'
|
||||
approverId?: string
|
||||
approvedAt?: string
|
||||
neededAt?: string
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
items: RequestItem[]
|
||||
}
|
||||
|
||||
export interface CatalogItem {
|
||||
id?: string
|
||||
name: string
|
||||
spec?: string
|
||||
unit?: string
|
||||
isActive?: boolean
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface ListQuery {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
applicantPhone?: string
|
||||
from?: string
|
||||
to?: string
|
||||
}
|
||||
|
||||
export interface CatalogQuery {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
isActive?: boolean
|
||||
q?: string
|
||||
}
|
||||
|
||||
// API 方法
|
||||
export default {
|
||||
// 创建临时申请
|
||||
createRequest(data: Partial<ConsumableRequest>) {
|
||||
return request('/api/consumable-temp-requests', {
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 查询申请列表
|
||||
getRequestList(params?: ListQuery) {
|
||||
return request('/api/consumable-temp-requests', {
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取申请详情
|
||||
getRequestDetail(id: string) {
|
||||
return request(`/api/consumable-temp-requests/${id}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 更新申请
|
||||
updateRequest(id: string, data: Partial<ConsumableRequest>) {
|
||||
return request(`/api/consumable-temp-requests/${id}`, {
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 删除申请
|
||||
deleteRequest(id: string) {
|
||||
return request(`/api/consumable-temp-requests/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
},
|
||||
|
||||
// 更新申请状态
|
||||
updateRequestStatus(id: string, data: {
|
||||
status: string
|
||||
approverId?: string
|
||||
remark?: string
|
||||
}) {
|
||||
return request(`/api/consumable-temp-requests/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 添加物料明细
|
||||
addRequestItem(requestId: string, data: Partial<RequestItem>) {
|
||||
return request(`/api/consumable-temp-requests/${requestId}/items`, {
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 更新物料明细
|
||||
updateRequestItem(itemId: string, data: Partial<RequestItem>) {
|
||||
return request(`/api/consumable-temp-items/${itemId}`, {
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 删除物料明细
|
||||
deleteRequestItem(itemId: string) {
|
||||
return request(`/api/consumable-temp-items/${itemId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
},
|
||||
|
||||
// 创建耗材目录
|
||||
createCatalog(data: Partial<CatalogItem>) {
|
||||
return request('/api/consumable-temp-catalog', {
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 查询目录列表
|
||||
getCatalogList(params?: CatalogQuery) {
|
||||
return request('/api/consumable-temp-catalog', {
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取目录详情
|
||||
getCatalogDetail(id: string) {
|
||||
return request(`/api/consumable-temp-catalog/${id}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 更新目录
|
||||
updateCatalog(id: string, data: Partial<CatalogItem>) {
|
||||
return request(`/api/consumable-temp-catalog/${id}`, {
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 停用目录
|
||||
deleteCatalog(id: string) {
|
||||
return request(`/api/consumable-temp-catalog/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,4 @@
|
||||
const BASE_URL = 'http://110.42.33.196:8089';//定义后端基础接口地址
|
||||
import config from '../../config.js'; // 引入配置文件
|
||||
|
||||
|
||||
|
||||
import { formatTime, formatDate, formatDateTime, BASE_URL, getHeaders } from './index.ts'; // 引入时间格式化函数和BASE_URL
|
||||
|
||||
// 根据手机号搜索用户列表
|
||||
export function searchUserByMobile(mobile) {
|
||||
@ -31,63 +27,6 @@ export function searchUserByMobile(mobile) {
|
||||
}
|
||||
|
||||
|
||||
//——————————————————————————————————————————————————————时间————————————————————————————————————————————————————
|
||||
// 数字补零工具函数(辅助日期格式化)
|
||||
const formatNumber = n => {
|
||||
n = n.toString()// 1. 将数字转为字符串(如 5 → "5",12 → "12")
|
||||
return n[1] ? n : '0' + n// 2. 补零逻辑:若字符串长度<2(如 "5"),则在前面加"0" → "05";否则直接返回(如 "12")
|
||||
}
|
||||
|
||||
//时间格式化函数(仅返回时分秒)
|
||||
const formatTime = date => {
|
||||
date = new Date(date);// 1. 将输入的日期参数(可能是时间戳/字符串)转为标准 Date 对象
|
||||
const hour = date.getHours()// 2. 获取小时(0-23
|
||||
const minute = date.getMinutes()// 3. 获取分钟(0-59)
|
||||
const second = date.getSeconds()// 4. 获取秒(0-59)
|
||||
// 5. 拼接为 "时:分:秒" 格式(调用 formatNumber 补零),如 "09:05:03
|
||||
return `${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
//日期格式化函数(仅返回年月日)
|
||||
const formatDate = date => {
|
||||
date = new Date(date);// 1. 转为标准 Date 对象
|
||||
const year = date.getFullYear()// 2. 获取完整年份(如 2024)
|
||||
const month = date.getMonth() + 1// 3. 获取月份(注意:Date 中月份是 0-11,需+1 才是实际月份,如 2→3月
|
||||
const day = date.getDate()// 4. 获取日期(1-31)
|
||||
// 5. 拼接为 "年-月-日" 格式(补零),如 "2024-03-15"
|
||||
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
|
||||
}
|
||||
|
||||
//完整日期时间格式化函数(年月日 + 时分秒)
|
||||
const formatDateTime = date => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
|
||||
//用户信息初始化与判断
|
||||
let staff = uni.getStorageSync('staff'); // 1. 从 UniApp 本地缓存中读取 "staff"(用户信息,如ID、医院ID、手机号等)
|
||||
let staffisnull = (staff == null || staff == undefined || staff == '');// 2. 判断用户信息是否为空(null/undefined/空字符串)
|
||||
|
||||
|
||||
//统一请求头配置函数(核心!!!!!!!!!!!!!!!!!!!!所有接口共用)
|
||||
export function getHeaders() {
|
||||
const staff = uni.getStorageSync('staff') || null// 1. 再次读取用户信息(避免缓存过期,确保最新)
|
||||
// console.log("用户信息:",staff)// 2. 控制台打印用户信息,用于开发调试
|
||||
const staffisnull = !staff // 3. 简化判断:用户信息为空则为 true,否则为 false
|
||||
// 4. 返回请求头对象(接口请求时会携带这些信息,供后端验证)
|
||||
return {
|
||||
'content-type': 'application/json',// ① 声明请求体格式为 JSON(后端需按 JSON 解析数据)
|
||||
//'Authorization': staffisnull ? null : `Bearer ${uni.getStorageSync('token')}`,
|
||||
'hosp-ID': staffisnull ? config.defaultHospId : staff.hosp_id,// ③ 医院ID:用户未登录用配置文件的默认值,登录后用用户所属医院ID
|
||||
'user-ID': staffisnull ? null : staff.id,// ④ 用户ID:用户未登录为 null,登录后用用户ID(后端用于识别请求用户)
|
||||
'datetime': formatDateTime(new Date()),// ⑤ 请求时间:当前完整日期时间(后端可用于校验请求时效性)
|
||||
'createby': staffisnull ? null : staff.id// ⑥ 创建人ID:与 user-ID 类似,用于后端记录操作人
|
||||
}////核心作用:统一所有接口的请求头格式,避免每个接口重复写头信息,同时支持 “登录 / 未登录” 状态的动态适配。
|
||||
}
|
||||
|
||||
//————————————————————————————————————————————————————固定资产相关————————————————————————————————————————————————————————
|
||||
//分页请求固定资产数据
|
||||
//作用:前端加载固定资产列表时调用(如 “资产列表页” 分页加载数据)。
|
||||
@ -104,10 +43,10 @@ export function fetchFixedAssets(pageNum, pageSize) {
|
||||
url: `${BASE_URL}/api/assetInformation/page?pageNum=${pageNum}&pageSize=${pageSize}`,// 接口路径:拼接基础地址+分页参数(pageNum=页码,pageSize=每页条数)
|
||||
method: 'POST',// 请求方法:POST
|
||||
data: {// 请求体:空查询条件(后端可能支持按条件筛选,此处默认无筛选)
|
||||
query:{},
|
||||
query: {},
|
||||
},
|
||||
|
||||
header:getHeaders(),// 请求头:调用上面的 getHeaders() 获取统一头信息
|
||||
header: getHeaders(),// 请求头:调用上面的 getHeaders() 获取统一头信息
|
||||
|
||||
success: (res) => { // 请求成功回调(后端有响应,无论业务成功与否)
|
||||
if (res.statusCode === 200) {// 若 HTTP 状态码为 200(表示网络请求成功)
|
||||
@ -133,10 +72,10 @@ export function searchFixedAssets(name) {
|
||||
url: `${BASE_URL}/api/assetInformation/list`,// 接口路径:资产搜索接口
|
||||
method: 'POST',
|
||||
data: {
|
||||
assetName:name // 请求体:传递搜索关键词(assetName=资产名称)
|
||||
assetName: name // 请求体:传递搜索关键词(assetName=资产名称)
|
||||
},
|
||||
|
||||
header:getHeaders(),
|
||||
header: getHeaders(),
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
@ -159,10 +98,10 @@ export function fetchAssetInfoById(id) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/assetInformation/list`, // 接口路径:获取单条资产信息
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
|
||||
"assetSn":id // 请求体:传递资产编号(assetSn=资产唯一编号)
|
||||
"assetSn": id // 请求体:传递资产编号(assetSn=资产唯一编号)
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -184,7 +123,7 @@ export function fetchMaintenanceInfoById(id) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/maintenance/getByAssetId/${id}`, // 接口路径:拼接资产ID(RESTful 风格)
|
||||
method: 'GET', // 请求方法:GET(此处用 GET 传递路径参数)
|
||||
header:getHeaders(),
|
||||
header: getHeaders(),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
@ -244,7 +183,7 @@ export function borrowInfo(borrowerId) {
|
||||
method: 'POST',
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
borrowerId:borrowerId // 请求体:传递借用人ID(查询该用户的所有借用记录)
|
||||
borrowerId: borrowerId // 请求体:传递借用人ID(查询该用户的所有借用记录)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
@ -268,7 +207,7 @@ export function returnAsset(id) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/borrow/returnAsset?assetId=${id}`, // 接口路径:拼接资产ID(URL参数)
|
||||
method: 'PUT', // 请求方法:PUT(通常用于“更新资源状态”,此处更新资产为“已归还”)
|
||||
header:getHeaders(),
|
||||
header: getHeaders(),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
@ -285,123 +224,17 @@ export function returnAsset(id) {
|
||||
|
||||
//————————————————————————————————————————————————业务接口函数(易耗品相关)——————————————————————————————————————————
|
||||
//易耗品领用
|
||||
//作用:前端提交 “易耗品领用表单” 时调用(完成领用申请)。
|
||||
export function getConsumption(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/add`, //接口路径:易耗品领用提交
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:易耗品领用所需参数(数量、组别ID、备注等)
|
||||
actualQuantity:addDTO.actualQuantity, // 实际领用数量
|
||||
groupId:addDTO.groupId,// 所属组别ID
|
||||
notes:addDTO.notes,// 备注
|
||||
planId:addDTO.planId,// 发放计划ID
|
||||
recipientMobile:addDTO.recipientMobile,// 领取人手机号
|
||||
skuId:addDTO.skuId// 易耗品SKU编号(唯一标识)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//根据条码查询sku Id
|
||||
export function getConsumptionSkuId(barcode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable/sku/list`,// 接口路径:条码查询SKU
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
barcode:barcode // 请求体:传递易耗品条码(通过条码获取对应的SKU ID)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//查询商品库存
|
||||
export function getConsumptionQuantity(goodsInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-inventory/findByCondition`, // 接口路径:库存查询
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:库存查询条件(SKU ID、仓库ID、位置ID)
|
||||
skuId:goodsInfo.skuId,
|
||||
locationId:goodsInfo.locationId,
|
||||
warehouseId:goodsInfo.warehouseId
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//——————————————————————————————————————————易耗品领用计划??????????????????????????????
|
||||
//获取组别信息
|
||||
export function getGroupsInfo(mobile) { //参数 mobile 表示“组长手机号”
|
||||
return new Promise((resolve, reject) => { // 返回 Promise 对象,支持异步调用(用 async/await 或 .then() 处理结果)
|
||||
// 调用 UniApp 的网络请求 API
|
||||
uni.request({
|
||||
// 请求地址:拼接基础地址 + 易耗品组别查询接口
|
||||
// 接口作用:根据条件查询易耗品组别(此处条件是组长手机号)
|
||||
url: `${BASE_URL}/api/consumable-groups/findByCondition`,
|
||||
method: 'POST',
|
||||
header:getHeaders(), // 请求头:使用统一的 getHeaders() 配置(携带用户、医院等信息)
|
||||
data:{ // 请求体:传递查询条件
|
||||
groupHeadMobile:mobile // 按“组长手机号”查询对应的组别(如组长管理的易耗品分组)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取发放计划信息
|
||||
export function getPlanInfo(id,pageNum, pageSize) {
|
||||
export function getPlanInfo(id, pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 请求地址:拼接基础地址 + 发放计划接口 + 分页参数(pageNum/pageSize)
|
||||
url: `${BASE_URL}/api/consumable-distribution-plan/findByCondition?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
hospId:id // 按“医院ID”查询该医院的易耗品发放计划(如“2024年Q1耗材发放计划”)
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
hospId: id // 按“医院ID”查询该医院的易耗品发放计划(如“2024年Q1耗材发放计划”)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
@ -423,8 +256,8 @@ export function getConsumptionDetail(id) {
|
||||
// 请求地址:拼接基础地址 + 领用详情接口 + 领用记录ID(RESTful风格)
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/app_getDistributeItem/${id}`,
|
||||
method: 'GET', // 请求方法:GET(查询详情常用GET)
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
// GET请求无需请求体,参数通过URL路径传递
|
||||
},
|
||||
success: (res) => {
|
||||
@ -446,8 +279,8 @@ export function consumptonConfirm(id) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/app_getDistributeItem/${id}`,
|
||||
method: 'POST', // 请求方法:POST(用于更新状态,确认领用
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -467,39 +300,7 @@ export function consumptonConfirm(id) {
|
||||
|
||||
|
||||
//————————————————————————————————————————————————资产盘点————————————————————————————————————————————————————————————
|
||||
//新增固定资产盘点
|
||||
export function addInventory(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/inventory/add`, // 接口路径:固定资产盘点记录提交接口
|
||||
method: 'POST', // 请求方法:POST(新增数据用POST)
|
||||
data: { // 请求体:盘点记录所需的核心参数(从前端盘点表单收集)
|
||||
assetId:addDTO.assetId, // 被盘点的固定资产ID
|
||||
assetName:addDTO.assetName, // 固定资产名称(冗余字段,便于后端校验)
|
||||
inventoryDate:addDTO.inventoryDate, // 盘点日期(格式化后的日期时间)
|
||||
inventoryManId:addDTO.inventoryManId, // 盘点人ID(当前操作用户ID)
|
||||
inventoryManMobile:addDTO.inventoryManMobile, // 盘点人手机号(用于身份校验)
|
||||
inventoryPic:addDTO.inventoryPic, // 盘点照片(可选,如资产实物照片,通常是Base64或图片URL)
|
||||
inventoryPlanId:addDTO.inventoryPlanId, // 所属盘点计划ID(关联到具体的盘点任务)
|
||||
inventoryResult:addDTO.inventoryResult, // 盘点结果(如“正常”“缺失”“损坏”等枚举值)
|
||||
note:addDTO.note // 盘点备注(可选,记录特殊情况,如“资产外观有划痕”)
|
||||
},
|
||||
|
||||
header:getHeaders(), // 统一请求头(携带用户、医院等信息)
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 成功时返回后端的盘点记录ID或成功信息
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取固定资产盘点计划信息
|
||||
export function getInventoryPlan(pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -507,8 +308,8 @@ export function getInventoryPlan(pageNum, pageSize) {
|
||||
// 接口路径:固定资产盘点计划分页接口,拼接页码(pageNum)和每页条数(pageSize)
|
||||
url: `${BASE_URL}/api/asset/inventoryPlan/page?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
// 请求体:空(后端可能默认返回当前用户有权限的盘点计划,无需额外筛选条件)
|
||||
},
|
||||
success: (res) => {
|
||||
@ -533,8 +334,8 @@ export function getInventoryDetailEnd(id) {
|
||||
url: `${BASE_URL}/api/asset/inventory/app_getInventoryPlan/${id}`,
|
||||
// 接口路径:拼接盘点计划ID,专门获取“进行中”计划的详情(与已结束计划接口区分开)
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -558,8 +359,8 @@ export function getInventoryDetailIng(id) {
|
||||
// 接口路径:拼接盘点计划ID,专门获取“进行中”计划的详情(与已结束计划接口区分开)
|
||||
url: `${BASE_URL}/api/asset/inventory/app_getInventoryPlan2/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -582,14 +383,14 @@ export function inventory(addDTO) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/inventory/add`, // 与“addInventory”接口路径相同,可能是后端兼容的简化版
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:简化后的参数(去掉了assetName、note等非必填项,inventoryDate注释后可能由后端自动填充)
|
||||
assetId:addDTO.assetId, // 被盘点资产ID(必填)
|
||||
inventoryManId:addDTO.inventoryManId, // 盘点人ID(必填)
|
||||
inventoryManMobile:addDTO.inventoryManMobile, // 盘点人手机号(必填)
|
||||
inventoryPic:addDTO.inventoryPic, // 盘点照片(可选)
|
||||
inventoryPlanId:addDTO.inventoryPlanId, // 所属计划ID(必填)
|
||||
inventoryResult:addDTO.inventoryResult, // 盘点结果(必填)
|
||||
header: getHeaders(),
|
||||
data: { // 请求体:简化后的参数(去掉了assetName、note等非必填项,inventoryDate注释后可能由后端自动填充)
|
||||
assetId: addDTO.assetId, // 被盘点资产ID(必填)
|
||||
inventoryManId: addDTO.inventoryManId, // 盘点人ID(必填)
|
||||
inventoryManMobile: addDTO.inventoryManMobile, // 盘点人手机号(必填)
|
||||
inventoryPic: addDTO.inventoryPic, // 盘点照片(可选)
|
||||
inventoryPlanId: addDTO.inventoryPlanId, // 所属计划ID(必填)
|
||||
inventoryResult: addDTO.inventoryResult, // 盘点结果(必填)
|
||||
//inventoryDate:addDTO.inventoryDate // 注释后,后端可能自动用当前时间作为盘点日期
|
||||
},
|
||||
success: (res) => {
|
||||
@ -615,8 +416,8 @@ export function getConsumpationPlan(pageNum, pageSize) {
|
||||
// 接口路径:易耗品盘点计划分页接口,名称区分于固定资产计划
|
||||
url: `${BASE_URL}/api/consumable-check-plan/findByConditionWithPage?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
// 空请求体(后端默认返回当前用户的易耗品盘点计划)
|
||||
},
|
||||
success: (res) => {
|
||||
@ -639,8 +440,11 @@ export function getConsumpationInventoryDetailEnd(id) {
|
||||
// 接口路径:拼接易耗品盘点计划ID,专门获取“已结束”计划的详情
|
||||
url: `${BASE_URL}/api/consumable-check-item/app_getInventoryPlan/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: {
|
||||
|
||||
...getHeaders()
|
||||
},
|
||||
data: {
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -664,8 +468,8 @@ export function getConsumpationInventoryDetailIng(id) {
|
||||
// 接口路径:拼接易耗品盘点计划ID,专门获取“进行中”计划的详情
|
||||
url: `${BASE_URL}/api/consumable-check-item/app_getInventoryPlan2/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
@ -689,12 +493,12 @@ export function addComInventory(addDTO) {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-check-item/add`, // 接口路径:易耗品盘点记录提交接口
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:易耗品盘点的核心参数(与固定资产盘点参数差异在于“数量”字段)
|
||||
checkPlanId:addDTO.checkPlanId, // 所属易耗品盘点计划ID
|
||||
checkResult:addDTO.checkResult, // 盘点结果(如“账实相符”“盘盈”“盘亏”)
|
||||
inventoryId:addDTO.inventoryId, // 库存ID(关联到具体的易耗品库存记录)
|
||||
quantity:addDTO.quantity // 实际盘点数量(易耗品需统计数量,固定资产通常是“有无”)
|
||||
header: getHeaders(),
|
||||
data: { // 请求体:易耗品盘点的核心参数(与固定资产盘点参数差异在于“数量”字段)
|
||||
checkPlanId: addDTO.checkPlanId, // 所属易耗品盘点计划ID
|
||||
checkResult: addDTO.checkResult, // 盘点结果(如“账实相符”“盘盈”“盘亏”)
|
||||
inventoryId: addDTO.inventoryId, // 库存ID(关联到具体的易耗品库存记录)
|
||||
quantity: addDTO.quantity // 实际盘点数量(易耗品需统计数量,固定资产通常是“有无”)
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
|
||||
49
src/pages/api/index.ts
Normal file
49
src/pages/api/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
const BASE_URL = 'http://110.42.33.196:8089';//定义后端基础接口地址
|
||||
import config from '../../config'; // 引入配置文件
|
||||
|
||||
const formatNumber = (n: number | string): string => {
|
||||
n = n.toString();// 1. 将数字转为字符串(如 5 → "5",12 → "12")
|
||||
return n[1] ? n : '0' + n;// 2. 补零逻辑:若字符串长度<2(如 "5"),则在前面加"0" → "05";否则直接返回(如 "12")
|
||||
}
|
||||
|
||||
const formatTime = (date: Date | string | number) => {
|
||||
date = new Date(date);// 1. 将输入的日期参数(可能是时间戳/字符串)转为标准 Date 对象
|
||||
const hour: number = date.getHours();// 2. 获取小时(0-23)
|
||||
const minute: number = date.getMinutes();// 3. 获取分钟(0-59)
|
||||
const second: number = date.getSeconds();// 4. 获取秒(0-59)
|
||||
// 5. 拼接为 "时:分:秒" 格式(调用 formatNumber 补零),如 "09:05:03"
|
||||
return `${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`;
|
||||
}
|
||||
|
||||
//日期格式化函数(仅返回年月日)
|
||||
const formatDate = (date: Date | string | number) => {
|
||||
date = new Date(date);// 1. 转为标准 Date 对象
|
||||
const year = date.getFullYear()// 2. 获取完整年份(如 2024)
|
||||
const month = date.getMonth() + 1// 3. 获取月份(注意:Date 中月份是 0-11,需+1 才是实际月份,如 2→3月
|
||||
const day = date.getDate()// 4. 获取日期(1-31)
|
||||
// 5. 拼接为 "年-月-日" 格式(补零),如 "2024-03-15"
|
||||
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
|
||||
}
|
||||
|
||||
//完整日期时间格式化函数(年月日 + 时分秒)
|
||||
const formatDateTime = (date: Date | string | number) => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
|
||||
//统一请求头配置函数(核心!!!!!!!!!!!!!!!!!!!!所有接口共用)
|
||||
function getHeaders() {
|
||||
const staff = uni.getStorageSync('staff') || null// 1. 再次读取用户信息(避免缓存过期,确保最新)
|
||||
// console.log("用户信息:",staff)// 2. 控制台打印用户信息,用于开发调试
|
||||
const staffisnull = !staff // 3. 简化判断:用户信息为空则为 true,否则为 false
|
||||
// 4. 返回请求头对象(接口请求时会携带这些信息,供后端验证)
|
||||
return {
|
||||
'content-type': 'application/json',// ① 声明请求体格式为 JSON(后端需按 JSON 解析数据)
|
||||
//'Authorization': staffisnull ? null : `Bearer ${uni.getStorageSync('token')}`,
|
||||
'hosp-ID': staffisnull ? config.defaultHospId : staff.hosp_id,// ③ 医院ID:用户未登录用配置文件的默认值,登录后用用户所属医院ID
|
||||
'user-ID': staffisnull ? null : staff.id,// ④ 用户ID:用户未登录为 null,登录后用用户ID(后端用于识别请求用户)
|
||||
'datetime': formatDateTime(new Date()),// ⑤ 请求时间:当前完整日期时间(后端可用于校验请求时效性)
|
||||
'createby': staffisnull ? null : staff.id// ⑥ 创建人ID:与 user-ID 类似,用于后端记录操作人
|
||||
}////核心作用:统一所有接口的请求头格式,避免每个接口重复写头信息,同时支持 “登录 / 未登录” 状态的动态适配。
|
||||
}
|
||||
|
||||
export { formatTime, formatDate, formatDateTime, BASE_URL, getHeaders };// 导出时间格式化函数、BASE_URL 和 getHeaders 供其他模块使用
|
||||
@ -1,46 +1,4 @@
|
||||
const BASE_URL = 'http://110.42.33.196:8089';
|
||||
const config = require('config.js');
|
||||
const formatTime = date => {
|
||||
date = new Date(date);
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return `${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`
|
||||
}
|
||||
|
||||
const formatNumber = n => {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
}
|
||||
|
||||
const formatDate = date => {
|
||||
date = new Date(date);
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
|
||||
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
|
||||
}
|
||||
|
||||
const formatDateTime = date => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
let staff = uni.getStorageSync('staff');
|
||||
let staffisnull = (staff == null || staff == undefined || staff == '');
|
||||
export function getHeaders() {
|
||||
const staff = uni.getStorageSync('staff') || null
|
||||
const staffisnull = !staff
|
||||
|
||||
return {
|
||||
'content-type': 'application/json',
|
||||
'Authorization': staffisnull ? null : `Bearer ${uni.getStorageSync('token')}`,
|
||||
'hosp-ID': staffisnull ? config.defaultHospId : staff.hosp_id,
|
||||
'user-ID': staffisnull ? null : staff.id,
|
||||
'datetime': formatDateTime(new Date()),
|
||||
'createby': staffisnull ? null : staff.id
|
||||
}
|
||||
}
|
||||
import { formatTime, formatDate, formatDateTime, BASE_URL ,getHeaders} from './index.js'; // 引入时间格式化函数和BASE_URL
|
||||
|
||||
//分页请求固定资产盘点信息
|
||||
export function fetchAssetInventory(pageNum, pageSize) {
|
||||
@ -49,10 +7,10 @@ export function fetchAssetInventory(pageNum, pageSize) {
|
||||
url: `${BASE_URL}/api/asset/inventory/page?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
query:{},
|
||||
query: {},
|
||||
},
|
||||
|
||||
header:getHeaders(),
|
||||
header: getHeaders(),
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
|
||||
54
src/pages/api/user.ts
Normal file
54
src/pages/api/user.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { formatTime, formatDate, formatDateTime, BASE_URL, getHeaders } from './index.js'; // 引入时间格式化函数和BASE_URL
|
||||
|
||||
//@ts-ignore
|
||||
import md5 from '@/utils/md5.js'
|
||||
|
||||
export function login(username: string, password: string): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
token: string;
|
||||
userLoginRequestVO: {
|
||||
id: number;
|
||||
user_name: string;
|
||||
user_login_name: string;
|
||||
user_type: string;
|
||||
user_pic_path: string | null;
|
||||
role_id: number | null;
|
||||
role_name: string | null;
|
||||
hosp_id: number | null;
|
||||
hospName: string | null;
|
||||
hospPic: string | null;
|
||||
hospCenterX: number | null;
|
||||
hospCenterY: number | null;
|
||||
role: string[] | null;
|
||||
assetsRole: number | null;
|
||||
roles: string[] | null;
|
||||
}
|
||||
}
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/myUser/login2`,
|
||||
method: 'POST',// 请求方法:POST
|
||||
data: {
|
||||
mobile: username,
|
||||
password: md5.hexMD5(username + password),
|
||||
role: '007'
|
||||
},
|
||||
|
||||
header: getHeaders(),
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data as any);
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
851
src/pages/consumables/temp-approve-detail.vue
Normal file
851
src/pages/consumables/temp-approve-detail.vue
Normal file
@ -0,0 +1,851 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content" v-if="!loading">
|
||||
<!-- 进度条 -->
|
||||
<view class="progress-section">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-step" :class="{ active: true, completed: ['APPROVED', 'COMPLETED', 'REJECTED', 'CANCELLED'].includes(detail.status || '') }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">{{ ['REJECTED', 'CANCELLED'].includes(detail.status || '') ? '✕' : '✓' }}</text>
|
||||
</view>
|
||||
<text class="step-label">待审批</text>
|
||||
</view>
|
||||
<view class="progress-line" :class="{ completed: ['APPROVED', 'COMPLETED'].includes(detail.status || '') }"></view>
|
||||
<view class="progress-step" :class="{ active: ['APPROVED', 'COMPLETED'].includes(detail.status || ''), completed: detail.status === 'COMPLETED' }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">✓</text>
|
||||
</view>
|
||||
<text class="step-label">待发放</text>
|
||||
</view>
|
||||
<view class="progress-line" :class="{ completed: detail.status === 'COMPLETED' }"></view>
|
||||
<view class="progress-step" :class="{ active: detail.status === 'COMPLETED', completed: detail.status === 'COMPLETED' }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">✓</text>
|
||||
</view>
|
||||
<text class="step-label">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<view class="status-card">
|
||||
<view class="status-badge" :class="`status-${detail.status?.toLowerCase()}`">
|
||||
{{ getStatusText(detail.status) }}
|
||||
</view>
|
||||
<text class="status-time">{{ formatTime(detail.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 申请人信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">申请人信息</view>
|
||||
<view class="applicant-card">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ getAvatarText(detail.applicantName) }}</text>
|
||||
</view>
|
||||
<view class="applicant-info">
|
||||
<text class="name">{{ detail.applicantName || '未填写' }}</text>
|
||||
<text class="phone">{{ detail.applicantPhone }}</text>
|
||||
<text class="department" v-if="detail.department">{{ detail.department }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">申请信息</view>
|
||||
<view class="info-card">
|
||||
<view class="info-row">
|
||||
<text class="label">申请原因:</text>
|
||||
<text class="value">{{ detail.reason || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">需要时间:</text>
|
||||
<text class="value">{{ detail.neededAt || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.approverId">
|
||||
<text class="label">审批人ID:</text>
|
||||
<text class="value">{{ detail.approverId }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.approvedAt">
|
||||
<text class="label">审批时间:</text>
|
||||
<text class="value">{{ formatTime(detail.approvedAt) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.remark">
|
||||
<text class="label">审批意见:</text>
|
||||
<text class="value remark">{{ detail.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 物料明细 -->
|
||||
<view class="section">
|
||||
<view class="section-title">物料明细({{ detail.items?.length || 0 }}项)</view>
|
||||
<view v-for="(item, index) in detail.items" :key="item.id" class="item-card">
|
||||
<view class="item-header">
|
||||
<text class="item-number">#{{ index + 1 }}</text>
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="item-details">
|
||||
<view class="detail-row" v-if="item.spec">
|
||||
<text class="detail-label">规格</text>
|
||||
<text class="detail-value">{{ item.spec }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">数量</text>
|
||||
<text class="detail-value highlight">{{ item.quantity }} {{ item.unit || '' }}</text>
|
||||
</view>
|
||||
<view class="detail-row" v-if="item.estimatedUnitCost">
|
||||
<text class="detail-label">预估单价</text>
|
||||
<text class="detail-value">¥{{ item.estimatedUnitCost }}</text>
|
||||
</view>
|
||||
<view class="detail-row" v-if="item.estimatedUnitCost && item.quantity">
|
||||
<text class="detail-label">小计</text>
|
||||
<text class="detail-value total">¥{{ (item.estimatedUnitCost * item.quantity).toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-remark" v-if="item.remark">
|
||||
<text class="remark-label">备注:</text>
|
||||
<text class="remark-text">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合计 -->
|
||||
<view class="section" v-if="totalCost > 0">
|
||||
<view class="total-card">
|
||||
<text class="total-label">预估总金额</text>
|
||||
<text class="total-value">¥{{ totalCost.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions" v-if="!loading && detail.status === 'PENDING'">
|
||||
<button class="btn-reject" @tap="handleReject">拒绝</button>
|
||||
<button class="btn-approve" @tap="handleApprove">通过</button>
|
||||
</view>
|
||||
<view class="footer-actions" v-if="!loading && detail.status === 'APPROVED'">
|
||||
<button class="btn-deliver" @tap="handleDeliver">确认发放</button>
|
||||
</view>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<view v-if="showApproveModal" class="modal-mask" @tap="closeApproveModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ approveType === 'APPROVED' ? '通过申请' : '拒绝申请' }}</text>
|
||||
<text class="modal-close" @tap="closeApproveModal">✕</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">审批意见</text>
|
||||
<textarea class="form-textarea" v-model="approveRemark"
|
||||
:placeholder="approveType === 'APPROVED' ? '请输入通过意见(可选)' : '请输入拒绝原因'"
|
||||
maxlength="200" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn-cancel" @tap="closeApproveModal">取消</button>
|
||||
<button class="btn-confirm"
|
||||
:class="approveType === 'REJECTED' ? 'btn-danger' : ''"
|
||||
@tap="confirmApprove"
|
||||
:disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '确认' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import consumableTempAPI, { type ConsumableRequest } from '@/pages/api/consumable-temp'
|
||||
import { refreshNow } from '@/utils/messageManager'
|
||||
|
||||
const detail = ref<ConsumableRequest>({
|
||||
applicantPhone: '',
|
||||
items: []
|
||||
})
|
||||
const loading = ref(true)
|
||||
const requestId = ref('')
|
||||
|
||||
// 审批相关
|
||||
const showApproveModal = ref(false)
|
||||
const approveType = ref<'APPROVED' | 'REJECTED' | 'COMPLETED'>('APPROVED')
|
||||
const approveRemark = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
'PENDING': '待审批',
|
||||
'APPROVED': '待发放',
|
||||
'REJECTED': '已拒绝',
|
||||
'CANCELLED': '已取消',
|
||||
'COMPLETED': '已完成'
|
||||
}
|
||||
return statusMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 计算总金额
|
||||
const totalCost = computed(() => {
|
||||
return detail.value.items?.reduce((sum, item) => {
|
||||
const cost = (item.estimatedUnitCost || 0) * (item.quantity || 0)
|
||||
return sum + cost
|
||||
}, 0) || 0
|
||||
})
|
||||
|
||||
// 加载详情
|
||||
async function loadDetail() {
|
||||
if (!requestId.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestDetail(requestId.value)
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
detail.value = (res.data as any).data
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载详情失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 获取头像文本
|
||||
function getAvatarText(name?: string) {
|
||||
if (!name) return '?'
|
||||
return name.substring(0, 1)
|
||||
}
|
||||
|
||||
// 通过申请
|
||||
function handleApprove() {
|
||||
approveType.value = 'APPROVED'
|
||||
approveRemark.value = ''
|
||||
showApproveModal.value = true
|
||||
}
|
||||
|
||||
// 拒绝申请
|
||||
function handleReject() {
|
||||
approveType.value = 'REJECTED'
|
||||
approveRemark.value = ''
|
||||
showApproveModal.value = true
|
||||
}
|
||||
|
||||
// 确认发放
|
||||
function handleDeliver() {
|
||||
uni.showModal({
|
||||
title: '确认发放',
|
||||
content: '确认已发放该申请的易耗品?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await consumableTempAPI.updateRequestStatus(requestId.value, {
|
||||
status: 'COMPLETED'
|
||||
})
|
||||
|
||||
if (result.statusCode === 200) {
|
||||
uni.showToast({
|
||||
title: '已完成发放',
|
||||
icon: 'success',
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
// 通知列表页和消息页需要刷新
|
||||
uni.$emit('tempApprove:changed')
|
||||
uni.$emit('tempMessage:changed')
|
||||
uni.navigateBack()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
// 刷新消息数量
|
||||
refreshNow()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('发放失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭审批弹窗
|
||||
function closeApproveModal() {
|
||||
showApproveModal.value = false
|
||||
approveRemark.value = ''
|
||||
}
|
||||
|
||||
// 确认审批
|
||||
async function confirmApprove() {
|
||||
if (submitting.value) return
|
||||
|
||||
// 拒绝时必须填写原因
|
||||
if (approveType.value === 'REJECTED' && !approveRemark.value.trim()) {
|
||||
uni.showToast({ title: '请输入拒绝原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const staff = uni.getStorageSync('staff')
|
||||
const res = await consumableTempAPI.updateRequestStatus(requestId.value, {
|
||||
status: approveType.value,
|
||||
approverId: staff?.id || undefined,
|
||||
remark: approveRemark.value.trim() || undefined
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
uni.showToast({
|
||||
title: approveType.value === 'APPROVED' ? '已通过' : '已拒绝',
|
||||
icon: 'success',
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
// 通知列表页和消息页需要刷新
|
||||
uni.$emit('tempApprove:changed')
|
||||
uni.$emit('tempMessage:changed')
|
||||
uni.navigateBack()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
closeApproveModal()
|
||||
// 刷新消息数量
|
||||
refreshNow()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('审批失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
const options = currentPage.options || {}
|
||||
|
||||
if (options.id) {
|
||||
requestId.value = options.id
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx 140rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 进度条区域 */
|
||||
.progress-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active .step-circle {
|
||||
background: #fff3e0;
|
||||
border: 3rpx solid #f57c00;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-circle {
|
||||
background: #4caf50;
|
||||
border: 3rpx solid #4caf50;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: #f57c00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-label {
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
flex: 1;
|
||||
height: 3rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-line.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #3a5ddd;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-time {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 12rpx 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.applicant-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: #3a5ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.applicant-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.phone {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.department {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-row .value.remark {
|
||||
color: #f57c00;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #4b7aff;
|
||||
padding: 4rpx 12rpx;
|
||||
background: #e3f2fd;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value.highlight {
|
||||
color: #4b7aff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.total {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.item-remark {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.remark-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-card {
|
||||
background: #f9a825;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(249, 168, 37, 0.2);
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-reject,
|
||||
.btn-approve,
|
||||
.btn-deliver {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background: #ef5350;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-deliver {
|
||||
background: #4caf50;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
max-height: 70vh;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
padding: 32rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 16rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-confirm.btn-danger {
|
||||
background: #ef5350;
|
||||
}
|
||||
|
||||
.btn-confirm[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
798
src/pages/consumables/temp-approve.vue
Normal file
798
src/pages/consumables/temp-approve.vue
Normal file
@ -0,0 +1,798 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">临时易耗品审批</text>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<view class="filter-bar">
|
||||
<view class="switch-item">
|
||||
<text class="switch-label">显示已完成</text>
|
||||
<switch :checked="showCompleted" @change="onShowCompletedChange" color="#3a5ddd" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<scroll-view scroll-y class="list-container" @scrolltolower="loadMore">
|
||||
<view v-if="list.length === 0 && !loading" class="empty">
|
||||
<image class="empty-icon" src="/static/icons/consumables-temp-approve.png" mode="aspectFit" />
|
||||
<text class="empty-text">暂无审批记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="item in list" :key="item.id" class="card" @tap="goToApprove(item.id)">
|
||||
<!-- 进度条 -->
|
||||
<view class="progress-bar">
|
||||
<view class="progress-step" :class="{ active: true, completed: ['APPROVED', 'COMPLETED', 'REJECTED', 'CANCELLED'].includes(item.status || '') }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">{{ ['REJECTED', 'CANCELLED'].includes(item.status || '') ? '✕' : '✓' }}</text>
|
||||
</view>
|
||||
<text class="step-label">待审批</text>
|
||||
</view>
|
||||
<view class="progress-line" :class="{ completed: ['APPROVED', 'COMPLETED'].includes(item.status || '') }"></view>
|
||||
<view class="progress-step" :class="{ active: ['APPROVED', 'COMPLETED'].includes(item.status || ''), completed: item.status === 'COMPLETED' }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">✓</text>
|
||||
</view>
|
||||
<text class="step-label">待发放</text>
|
||||
</view>
|
||||
<view class="progress-line" :class="{ completed: item.status === 'COMPLETED' }"></view>
|
||||
<view class="progress-step" :class="{ active: item.status === 'COMPLETED', completed: item.status === 'COMPLETED' }">
|
||||
<view class="step-circle">
|
||||
<text class="step-icon">✓</text>
|
||||
</view>
|
||||
<text class="step-label">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-header">
|
||||
<view class="status-badge" :class="`status-${item.status?.toLowerCase()}`">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
<text class="time">{{ formatTime(item.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="applicant-info">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ getAvatarText(item.applicantName) }}</text>
|
||||
</view>
|
||||
<view class="applicant-details">
|
||||
<text class="name">{{ item.applicantName || '未填写' }}</text>
|
||||
<text class="phone">{{ item.applicantPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-grid">
|
||||
<view class="grid-item">
|
||||
<text class="grid-label">部门</text>
|
||||
<text class="grid-value">{{ item.department || '-' }}</text>
|
||||
</view>
|
||||
<view class="grid-item">
|
||||
<text class="grid-label">物料数量</text>
|
||||
<text class="grid-value highlight">{{ item.items?.length || 0 }} 项</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="reason-box">
|
||||
<text class="reason-label">申请原因:</text>
|
||||
<text class="reason-text">{{ item.reason || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer" v-if="item.status === 'PENDING'">
|
||||
<button class="btn-reject" @tap.stop="handleReject(item.id)">拒绝</button>
|
||||
<button class="btn-approve" @tap.stop="handleApprove(item.id)">通过</button>
|
||||
</view>
|
||||
<view class="card-footer" v-else-if="item.status === 'APPROVED'">
|
||||
<button class="btn-deliver" @tap.stop="handleDeliver(item.id)">确认发放</button>
|
||||
</view>
|
||||
<view class="card-footer" v-else>
|
||||
<text class="detail-link">查看详情 →</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && list.length > 0" class="no-more">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<view v-if="showApproveModal" class="modal-mask" @tap="closeApproveModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ approveType === 'APPROVED' ? '通过申请' : '拒绝申请' }}</text>
|
||||
<text class="modal-close" @tap="closeApproveModal">✕</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">审批意见</text>
|
||||
<textarea class="form-textarea" v-model="approveRemark"
|
||||
:placeholder="approveType === 'APPROVED' ? '请输入通过意见(可选)' : '请输入拒绝原因'"
|
||||
maxlength="200" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn-cancel" @tap="closeApproveModal">取消</button>
|
||||
<button class="btn-confirm"
|
||||
:class="approveType === 'REJECTED' ? 'btn-danger' : ''"
|
||||
@tap="confirmApprove"
|
||||
:disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '确认' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import consumableTempAPI, { type ConsumableRequest } from '@/pages/api/consumable-temp'
|
||||
import { refreshNow } from '@/utils/messageManager'
|
||||
|
||||
// 数据状态
|
||||
const list = ref<ConsumableRequest[]>([])
|
||||
const loading = ref(false)
|
||||
const showCompleted = ref(false) // 是否显示已完成
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const hasMore = ref(true)
|
||||
const needRefresh = ref(false) // 是否需要刷新
|
||||
|
||||
// 审批相关
|
||||
const showApproveModal = ref(false)
|
||||
const approveType = ref<'APPROVED' | 'REJECTED' | 'COMPLETED'>('APPROVED')
|
||||
const approveRemark = ref('')
|
||||
const currentApproveId = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
// 加载列表
|
||||
async function loadList(reset = false) {
|
||||
if (loading.value) return
|
||||
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
list.value = []
|
||||
}
|
||||
|
||||
if (!hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
let newData = (res.data as any).data
|
||||
|
||||
// 根据开关过滤数据:默认不显示 COMPLETED
|
||||
if (!showCompleted.value) {
|
||||
newData = newData.filter((item: ConsumableRequest) => item.status !== 'COMPLETED')
|
||||
}
|
||||
|
||||
list.value = reset ? newData : [...list.value, ...newData]
|
||||
hasMore.value = newData.length >= pageSize.value
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载列表失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示已完成开关改变
|
||||
function onShowCompletedChange(e: any) {
|
||||
showCompleted.value = e.detail.value
|
||||
loadList(true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (!loading.value && hasMore.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
'PENDING': '待审批',
|
||||
'APPROVED': '待发放',
|
||||
'REJECTED': '已拒绝',
|
||||
'CANCELLED': '已取消',
|
||||
'COMPLETED': '已完成'
|
||||
}
|
||||
return statusMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 获取头像文本
|
||||
function getAvatarText(name?: string) {
|
||||
if (!name) return '?'
|
||||
return name.substring(0, 1)
|
||||
}
|
||||
|
||||
// 通过申请
|
||||
function handleApprove(id?: string) {
|
||||
if (!id) return
|
||||
currentApproveId.value = id
|
||||
approveType.value = 'APPROVED'
|
||||
approveRemark.value = ''
|
||||
showApproveModal.value = true
|
||||
}
|
||||
|
||||
// 拒绝申请
|
||||
function handleReject(id?: string) {
|
||||
if (!id) return
|
||||
currentApproveId.value = id
|
||||
approveType.value = 'REJECTED'
|
||||
approveRemark.value = ''
|
||||
showApproveModal.value = true
|
||||
}
|
||||
|
||||
// 确认发放
|
||||
function handleDeliver(id?: string) {
|
||||
if (!id) return
|
||||
uni.showModal({
|
||||
title: '确认发放',
|
||||
content: '确认已发放该申请的易耗品?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await consumableTempAPI.updateRequestStatus(id, {
|
||||
status: 'COMPLETED'
|
||||
})
|
||||
|
||||
if (result.statusCode === 200) {
|
||||
uni.showToast({ title: '已完成发放', icon: 'success' })
|
||||
loadList(true)
|
||||
// 刷新消息数量和通知消息页
|
||||
refreshNow()
|
||||
uni.$emit('tempMessage:changed')
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('发放失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭审批弹窗
|
||||
function closeApproveModal() {
|
||||
showApproveModal.value = false
|
||||
approveRemark.value = ''
|
||||
currentApproveId.value = ''
|
||||
}
|
||||
|
||||
// 确认审批
|
||||
async function confirmApprove() {
|
||||
if (submitting.value) return
|
||||
|
||||
// 拒绝时必须填写原因
|
||||
if (approveType.value === 'REJECTED' && !approveRemark.value.trim()) {
|
||||
uni.showToast({ title: '请输入拒绝原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const staff = uni.getStorageSync('staff')
|
||||
const res = await consumableTempAPI.updateRequestStatus(currentApproveId.value, {
|
||||
status: approveType.value,
|
||||
approverId: staff?.id || undefined,
|
||||
remark: approveRemark.value.trim() || undefined
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
uni.showToast({
|
||||
title: approveType.value === 'APPROVED' ? '已通过' : '已拒绝',
|
||||
icon: 'success'
|
||||
})
|
||||
closeApproveModal()
|
||||
loadList(true)
|
||||
// 刷新消息数量和通知消息页
|
||||
refreshNow()
|
||||
uni.$emit('tempMessage:changed')
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('审批失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到详情
|
||||
function goToApprove(id?: string) {
|
||||
if (!id) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/consumables/temp-approve-detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onLoad(() => {
|
||||
loadList(true)
|
||||
// 监听审批变更事件
|
||||
uni.$on('tempApprove:changed', () => {
|
||||
needRefresh.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (needRefresh.value) {
|
||||
loadList(true)
|
||||
needRefresh.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off('tempApprove:changed')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1;
|
||||
padding: 0 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active .step-circle {
|
||||
background: #fff3e0;
|
||||
border: 3rpx solid #f57c00;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-circle {
|
||||
background: #4caf50;
|
||||
border: 3rpx solid #4caf50;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: #f57c00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-step.completed .step-label {
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
flex: 1;
|
||||
height: 3rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 8rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-line.completed {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.applicant-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: #3a5ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.applicant-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.phone {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
padding: 16rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.grid-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid-value.highlight {
|
||||
color: #4b7aff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reason-box {
|
||||
padding: 16rpx;
|
||||
background: #fffbf0;
|
||||
border-left: 4rpx solid #ffa726;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.reason-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.reason-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-reject,
|
||||
.btn-approve,
|
||||
.btn-deliver {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-deliver {
|
||||
background: #4caf50;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
font-size: 26rpx;
|
||||
color: #4b7aff;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
max-height: 70vh;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
padding: 32rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 16rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-confirm.btn-danger {
|
||||
background: #ef5350;
|
||||
}
|
||||
|
||||
.btn-confirm[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
588
src/pages/consumables/temp-catalog.vue
Normal file
588
src/pages/consumables/temp-catalog.vue
Normal file
@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">耗材种类管理</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-box">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="搜索物料名称或规格" @confirm="loadList(true)" />
|
||||
</view>
|
||||
<button class="btn-add" @tap="showAddModal">+ 新增</button>
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<scroll-view scroll-y class="list-container" @scrolltolower="loadMore">
|
||||
<view v-if="list.length === 0 && !loading" class="empty">
|
||||
<text class="empty-text">暂无耗材种类</text>
|
||||
</view>
|
||||
|
||||
<view v-for="item in list" :key="item.id" class="catalog-card">
|
||||
<view class="card-main">
|
||||
<view class="catalog-info">
|
||||
<text class="catalog-name">{{ item.name }}</text>
|
||||
<view class="catalog-meta">
|
||||
<text v-if="item.spec" class="meta-item">规格: {{ item.spec }}</text>
|
||||
<text v-if="item.unit" class="meta-item">单位: {{ item.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="catalog-status" :class="item.isActive ? 'active' : 'inactive'">
|
||||
{{ item.isActive ? '启用' : '停用' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="time">{{ formatTime(item.updatedAt) }}</text>
|
||||
<view class="actions">
|
||||
<button class="btn-edit" @tap="handleEdit(item)">编辑</button>
|
||||
<button v-if="item.isActive" class="btn-disable" @tap="handleDisable(item.id)">停用</button>
|
||||
<button v-else class="btn-enable" @tap="handleEnable(item.id)">启用</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && list.length > 0" class="no-more">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 编辑/新增弹窗 -->
|
||||
<view v-if="showModal" class="modal-mask" @tap="closeModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEdit ? '编辑种类' : '新增种类' }}</text>
|
||||
<text class="modal-close" @tap="closeModal">✕</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label required">物料名称</text>
|
||||
<input class="form-input" v-model="formData.name" placeholder="请输入物料名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">规格</text>
|
||||
<input class="form-input" v-model="formData.spec" placeholder="请输入规格(可选)" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">单位</text>
|
||||
<input class="form-input" v-model="formData.unit" placeholder="请输入单位(可选)" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">状态</text>
|
||||
<view class="switch-box">
|
||||
<switch :checked="formData.isActive" @change="onSwitchChange" color="#667eea" />
|
||||
<text class="switch-label">{{ formData.isActive ? '启用' : '停用' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn-cancel" @tap="closeModal">取消</button>
|
||||
<button class="btn-confirm" @tap="handleSubmit" :disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '确认' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import consumableTempAPI, { type CatalogItem } from '@/pages/api/consumable-temp'
|
||||
|
||||
// 数据状态
|
||||
const list = ref<CatalogItem[]>([])
|
||||
const loading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 弹窗相关
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formData = ref<CatalogItem>({
|
||||
name: '',
|
||||
spec: '',
|
||||
unit: '',
|
||||
isActive: true
|
||||
})
|
||||
const submitting = ref(false)
|
||||
|
||||
// 加载列表
|
||||
async function loadList(reset = false) {
|
||||
if (loading.value) return
|
||||
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
list.value = []
|
||||
}
|
||||
|
||||
if (!hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getCatalogList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
q: searchKeyword.value || undefined
|
||||
})
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
const newData = (res.data as any).data
|
||||
list.value = reset ? newData : [...list.value, ...newData]
|
||||
hasMore.value = newData.length >= pageSize.value
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载列表失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (!loading.value && hasMore.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 显示新增弹窗
|
||||
function showAddModal() {
|
||||
isEdit.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
spec: '',
|
||||
unit: '',
|
||||
isActive: true
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(item: CatalogItem) {
|
||||
isEdit.value = true
|
||||
formData.value = { ...item }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
// 开关改变
|
||||
function onSwitchChange(e: any) {
|
||||
formData.value.isActive = e.detail.value
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!formData.value.name) {
|
||||
uni.showToast({ title: '请输入物料名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (submitting.value) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
let res
|
||||
if (isEdit.value && formData.value.id) {
|
||||
res = await consumableTempAPI.updateCatalog(formData.value.id, formData.value)
|
||||
} else {
|
||||
res = await consumableTempAPI.createCatalog(formData.value)
|
||||
}
|
||||
|
||||
if (res.statusCode === 200 || res.statusCode === 201) {
|
||||
uni.showToast({
|
||||
title: isEdit.value ? '更新成功' : '创建成功',
|
||||
icon: 'success'
|
||||
})
|
||||
closeModal()
|
||||
loadList(true)
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('提交失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停用
|
||||
function handleDisable(id?: string) {
|
||||
if (!id) return
|
||||
|
||||
uni.showModal({
|
||||
title: '确认停用',
|
||||
content: '停用后该种类将不会出现在申请选择列表中',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await consumableTempAPI.deleteCatalog(id)
|
||||
if (result.statusCode === 204) {
|
||||
uni.showToast({ title: '已停用', icon: 'success' })
|
||||
loadList(true)
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('停用失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 启用
|
||||
async function handleEnable(id?: string) {
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.updateCatalog(id, { isActive: true })
|
||||
if (res.statusCode === 200) {
|
||||
uni.showToast({ title: '已启用', icon: 'success' })
|
||||
loadList(true)
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('启用失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
padding: 0 24rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1;
|
||||
padding: 0 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.catalog-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.catalog-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.catalog-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.catalog-meta {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
padding: 4rpx 12rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.catalog-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.catalog-status.active {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.catalog-status.inactive {
|
||||
background: #f5f5f5;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.btn-edit,
|
||||
.btn-disable,
|
||||
.btn-enable {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.btn-disable {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.btn-enable {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-label.required::before {
|
||||
content: '*';
|
||||
color: #ff4d4f;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.switch-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-confirm[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
613
src/pages/consumables/temp-request-create.vue
Normal file
613
src/pages/consumables/temp-request-create.vue
Normal file
@ -0,0 +1,613 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content">
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text>基本信息</text>
|
||||
</view>
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label required">申请人姓名</text>
|
||||
<input class="form-input" v-model="formData.applicantName" placeholder="请输入申请人姓名" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label required">联系电话</text>
|
||||
<input class="form-input" v-model="formData.applicantPhone" type="number" placeholder="请输入联系电话" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">部门</text>
|
||||
<input class="form-input" v-model="formData.department" placeholder="请输入部门" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">申请原因</text>
|
||||
<textarea class="form-textarea" v-model="formData.reason" placeholder="请输入申请原因" maxlength="200" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">需要时间</text>
|
||||
<picker mode="date" :value="formData.neededAt" @change="onDateChange">
|
||||
<view class="form-picker">
|
||||
<text>{{ formData.neededAt || '请选择需要时间' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 物料明细 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text>物料明细</text>
|
||||
<button class="btn-add" @tap="addItem">+ 添加物料</button>
|
||||
</view>
|
||||
|
||||
<view v-if="formData.items.length === 0" class="empty-items">
|
||||
<text>暂无物料,请点击上方按钮添加</text>
|
||||
</view>
|
||||
|
||||
<view v-for="(item, index) in formData.items" :key="index" class="item-card">
|
||||
<view class="item-header">
|
||||
<text class="item-number">#{{ index + 1 }}</text>
|
||||
<button class="btn-delete" @tap="deleteItem(index)">删除</button>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label required">物料名称</text>
|
||||
<view class="form-row">
|
||||
<input class="form-input flex-1" v-model="item.name" placeholder="请输入物料名称" />
|
||||
<button class="btn-catalog" @tap="selectFromCatalog(index)">从目录选择</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">规格</text>
|
||||
<input class="form-input" v-model="item.spec" placeholder="请输入规格" />
|
||||
</view>
|
||||
|
||||
<view class="form-row-group">
|
||||
<view class="form-item flex-1">
|
||||
<text class="form-label required">数量</text>
|
||||
<input class="form-input" v-model.number="item.quantity" type="digit" placeholder="数量" />
|
||||
</view>
|
||||
<view class="form-item flex-1">
|
||||
<text class="form-label">单位</text>
|
||||
<input class="form-input" v-model="item.unit" placeholder="单位" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">预估单价</text>
|
||||
<input class="form-input" v-model.number="item.estimatedUnitCost" type="digit" placeholder="请输入预估单价" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="item.remark" placeholder="请输入备注" maxlength="100" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions">
|
||||
<button class="btn-secondary" @tap="goBack">取消</button>
|
||||
<button class="btn-primary" @tap="submitForm" :disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '提交申请' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 目录选择弹窗 -->
|
||||
<view v-if="showCatalogModal" class="modal-mask" @tap="closeCatalog">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择物料</text>
|
||||
<text class="modal-close" @tap="closeCatalog">✕</text>
|
||||
</view>
|
||||
<view class="modal-search">
|
||||
<input class="search-input" v-model="catalogSearch" placeholder="搜索物料名称" />
|
||||
</view>
|
||||
<scroll-view scroll-y class="modal-list">
|
||||
<view v-for="catalog in filteredCatalog" :key="catalog.id"
|
||||
class="catalog-item"
|
||||
@tap="selectCatalog(catalog)">
|
||||
<view class="catalog-name">{{ catalog.name }}</view>
|
||||
<view class="catalog-info">
|
||||
<text v-if="catalog.spec">{{ catalog.spec }}</text>
|
||||
<text v-if="catalog.unit">{{ catalog.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredCatalog.length === 0" class="empty">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import consumableTempAPI, { type RequestItem, type CatalogItem } from '@/pages/api/consumable-temp'
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
applicantName: '',
|
||||
applicantPhone: '',
|
||||
department: '',
|
||||
reason: '',
|
||||
neededAt: '',
|
||||
items: [] as RequestItem[]
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const currentItemIndex = ref(-1)
|
||||
const showCatalogModal = ref(false)
|
||||
const catalogList = ref<CatalogItem[]>([])
|
||||
const catalogSearch = ref('')
|
||||
|
||||
// 过滤后的目录
|
||||
const filteredCatalog = computed(() => {
|
||||
if (!catalogSearch.value) return catalogList.value
|
||||
const keyword = catalogSearch.value.toLowerCase()
|
||||
return catalogList.value.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.spec?.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 加载目录
|
||||
async function loadCatalog() {
|
||||
try {
|
||||
const res = await consumableTempAPI.getCatalogList({
|
||||
isActive: true,
|
||||
pageSize: 99
|
||||
})
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
catalogList.value = (res.data as any).data
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载目录失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 日期改变
|
||||
function onDateChange(e: any) {
|
||||
formData.value.neededAt = e.detail.value
|
||||
}
|
||||
|
||||
// 添加物料
|
||||
function addItem() {
|
||||
formData.value.items.push({
|
||||
name: '',
|
||||
spec: '',
|
||||
quantity: 1,
|
||||
unit: '',
|
||||
estimatedUnitCost: 0,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除物料
|
||||
function deleteItem(index: number) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个物料吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
formData.value.items.splice(index, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 从目录选择
|
||||
function selectFromCatalog(index: number) {
|
||||
currentItemIndex.value = index
|
||||
showCatalogModal.value = true
|
||||
}
|
||||
|
||||
// 选择目录项
|
||||
function selectCatalog(catalog: CatalogItem) {
|
||||
if (currentItemIndex.value >= 0) {
|
||||
const item = formData.value.items[currentItemIndex.value]
|
||||
item.name = catalog.name
|
||||
item.spec = catalog.spec || ''
|
||||
item.unit = catalog.unit || ''
|
||||
item.catalogId = catalog.id
|
||||
}
|
||||
closeCatalog()
|
||||
}
|
||||
|
||||
// 关闭目录弹窗
|
||||
function closeCatalog() {
|
||||
showCatalogModal.value = false
|
||||
catalogSearch.value = ''
|
||||
currentItemIndex.value = -1
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
if (!formData.value.applicantPhone) {
|
||||
uni.showToast({ title: '请输入联系电话', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (formData.value.applicantPhone.length < 3) {
|
||||
uni.showToast({ title: '联系电话格式不正确', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (formData.value.items.length === 0) {
|
||||
uni.showToast({ title: '请至少添加一项物料', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < formData.value.items.length; i++) {
|
||||
const item = formData.value.items[i]
|
||||
if (!item.name) {
|
||||
uni.showToast({ title: `第${i + 1}项物料名称不能为空`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!item.quantity || item.quantity <= 0) {
|
||||
uni.showToast({ title: `第${i + 1}项物料数量必须大于0`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
if (!validateForm()) return
|
||||
|
||||
if (submitting.value) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.createRequest(formData.value)
|
||||
|
||||
if (res.statusCode === 201) {
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success',
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
// 通知列表页需要刷新
|
||||
uni.$emit('tempRequest:changed')
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('提交失败:', err)
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
function goBack() {
|
||||
if (formData.value.items.length > 0 || formData.value.applicantName) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要放弃当前编辑吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCatalog()
|
||||
// 自动填充当前用户信息
|
||||
const staff = uni.getStorageSync('staff')
|
||||
if (staff) {
|
||||
formData.value.applicantName = staff.name || ''
|
||||
formData.value.applicantPhone = staff.phone || ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx 140rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12rpx 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
padding: 8rpx 20rpx;
|
||||
margin: 0;
|
||||
height: 56rpx;
|
||||
line-height: 40rpx;
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-card,
|
||||
.item-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.item-card {
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #4b7aff;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 8rpx 16rpx;
|
||||
height: 48rpx;
|
||||
line-height: 32rpx;
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-label.required::before {
|
||||
content: '*';
|
||||
color: #ff4d4f;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-picker {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row .flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-catalog {
|
||||
padding: 0 20rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-row-group {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.form-row-group .form-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-items {
|
||||
padding: 80rpx 0;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-search {
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.modal-list {
|
||||
flex: 1;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.catalog-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.catalog-info {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 80rpx 0;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
456
src/pages/consumables/temp-request-detail.vue
Normal file
456
src/pages/consumables/temp-request-detail.vue
Normal file
@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view scroll-y class="content" v-if="!loading">
|
||||
<!-- 状态卡片 -->
|
||||
<view class="status-card">
|
||||
<view class="status-badge" :class="`status-${detail.status?.toLowerCase()}`">
|
||||
{{ getStatusText(detail.status) }}
|
||||
</view>
|
||||
<text class="status-time">{{ formatTime(detail.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">基本信息</view>
|
||||
<view class="info-card">
|
||||
<view class="info-row">
|
||||
<text class="label">申请人:</text>
|
||||
<text class="value">{{ detail.applicantName || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">联系电话:</text>
|
||||
<text class="value">{{ detail.applicantPhone }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">部门:</text>
|
||||
<text class="value">{{ detail.department || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">申请原因:</text>
|
||||
<text class="value">{{ detail.reason || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">需要时间:</text>
|
||||
<text class="value">{{ detail.neededAt || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.approverId">
|
||||
<text class="label">审批人ID:</text>
|
||||
<text class="value">{{ detail.approverId }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.approvedAt">
|
||||
<text class="label">审批时间:</text>
|
||||
<text class="value">{{ formatTime(detail.approvedAt) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.remark">
|
||||
<text class="label">备注:</text>
|
||||
<text class="value">{{ detail.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 物料明细 -->
|
||||
<view class="section">
|
||||
<view class="section-title">物料明细({{ detail.items?.length || 0 }}项)</view>
|
||||
<view v-for="(item, index) in detail.items" :key="item.id" class="item-card">
|
||||
<view class="item-header">
|
||||
<text class="item-number">#{{ index + 1 }}</text>
|
||||
</view>
|
||||
<view class="item-body">
|
||||
<view class="item-name">{{ item.name }}</view>
|
||||
<view class="item-details">
|
||||
<view class="detail-item" v-if="item.spec">
|
||||
<text class="detail-label">规格:</text>
|
||||
<text class="detail-value">{{ item.spec }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">数量:</text>
|
||||
<text class="detail-value highlight">{{ item.quantity }} {{ item.unit || '' }}</text>
|
||||
</view>
|
||||
<view class="detail-item" v-if="item.estimatedUnitCost">
|
||||
<text class="detail-label">预估单价:</text>
|
||||
<text class="detail-value">¥{{ item.estimatedUnitCost }}</text>
|
||||
</view>
|
||||
<view class="detail-item" v-if="item.estimatedUnitCost && item.quantity">
|
||||
<text class="detail-label">小计:</text>
|
||||
<text class="detail-value total">¥{{ (item.estimatedUnitCost * item.quantity).toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="detail-item full-width" v-if="item.remark">
|
||||
<text class="detail-label">备注:</text>
|
||||
<text class="detail-value">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合计 -->
|
||||
<view class="section" v-if="totalCost > 0">
|
||||
<view class="total-card">
|
||||
<text class="total-label">预估总金额</text>
|
||||
<text class="total-value">¥{{ totalCost.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions" v-if="!loading && detail.status === 'PENDING'">
|
||||
<button class="btn-danger" @tap="handleCancel">取消申请</button>
|
||||
<button class="btn-primary" @tap="handleEdit">编辑</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import consumableTempAPI, { type ConsumableRequest } from '@/pages/api/consumable-temp'
|
||||
|
||||
const detail = ref<ConsumableRequest>({
|
||||
applicantPhone: '',
|
||||
items: []
|
||||
})
|
||||
const loading = ref(true)
|
||||
const requestId = ref('')
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: 'PENDING', label: '待审批' },
|
||||
{ value: 'APPROVED', label: '待发放' },
|
||||
{ value: 'REJECTED', label: '已拒绝' },
|
||||
{ value: 'CANCELLED', label: '已取消' },
|
||||
{ value: 'COMPLETED', label: '已完成' }
|
||||
]
|
||||
|
||||
// 计算总金额
|
||||
const totalCost = computed(() => {
|
||||
return detail.value.items?.reduce((sum, item) => {
|
||||
const cost = (item.estimatedUnitCost || 0) * (item.quantity || 0)
|
||||
return sum + cost
|
||||
}, 0) || 0
|
||||
})
|
||||
|
||||
// 加载详情
|
||||
async function loadDetail() {
|
||||
if (!requestId.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestDetail(requestId.value)
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
detail.value = (res.data as any).data
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载详情失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string) {
|
||||
const item = statusOptions.find(s => s.value === status)
|
||||
return item?.label || '未知'
|
||||
}
|
||||
|
||||
// 取消申请
|
||||
function handleCancel() {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消这个申请吗?取消后不可恢复。',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await consumableTempAPI.updateRequestStatus(requestId.value, {
|
||||
status: 'CANCELLED'
|
||||
})
|
||||
|
||||
if (result.statusCode === 200) {
|
||||
uni.showToast({
|
||||
title: '已取消',
|
||||
icon: 'success',
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
// 通知列表页需要刷新
|
||||
uni.$emit('tempRequest:changed')
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('取消失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑申请
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/consumables/temp-request-edit?id=${requestId.value}`
|
||||
})
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
const options = currentPage.options || {}
|
||||
|
||||
if (options.id) {
|
||||
requestId.value = options.id
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx 140rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #3a5ddd;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-time {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 12rpx 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #4b7aff;
|
||||
}
|
||||
|
||||
.item-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.detail-item.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-value.highlight {
|
||||
color: #4b7aff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.total {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-card {
|
||||
background: #f9a825;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(249, 168, 37, 0.2);
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-danger,
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef5350;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
438
src/pages/consumables/temp-request.vue
Normal file
438
src/pages/consumables/temp-request.vue
Normal file
@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">临时易耗品申请</text>
|
||||
<view class="actions">
|
||||
<button class="btn-outline" @tap="goToCatalog">种类管理</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<view class="filter-bar">
|
||||
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
|
||||
<view class="filter-item">
|
||||
<text>{{ currentStatus?.label || '全部状态' }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<scroll-view scroll-y class="list-container" @scrolltolower="loadMore">
|
||||
<view v-if="list.length === 0 && !loading" class="empty">
|
||||
<image class="empty-icon" src="/static/icons/asset-inventory.png" mode="aspectFit" />
|
||||
<text class="empty-text">暂无申请记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="item in list" :key="item.id" class="card" @tap="goToDetail(item.id)">
|
||||
<view class="card-header">
|
||||
<view class="status-badge" :class="`status-${item.status?.toLowerCase()}`">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
<text class="time">{{ formatTime(item.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">申请人:</text>
|
||||
<text class="value">{{ item.applicantName || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">电话:</text>
|
||||
<text class="value">{{ item.applicantPhone }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">部门:</text>
|
||||
<text class="value">{{ item.department || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">申请原因:</text>
|
||||
<text class="value reason">{{ item.reason || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">物料数量:</text>
|
||||
<text class="value highlight">{{ item.items?.length || 0 }} 项</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="detail-link">查看详情 →</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && list.length > 0" class="no-more">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions">
|
||||
<button class="btn-primary" @tap="goToCreate">
|
||||
<text class="btn-icon">+</text>
|
||||
新建申请
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import consumableTempAPI, { type ConsumableRequest } from '@/pages/api/consumable-temp'
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'PENDING', label: '待审批' },
|
||||
{ value: 'APPROVED', label: '待发放' },
|
||||
{ value: 'REJECTED', label: '已拒绝' },
|
||||
{ value: 'CANCELLED', label: '已取消' },
|
||||
{ value: 'COMPLETED', label: '已完成' }
|
||||
]
|
||||
|
||||
// 数据状态
|
||||
const list = ref<ConsumableRequest[]>([])
|
||||
const loading = ref(false)
|
||||
const currentStatus = ref(statusOptions[0])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const hasMore = ref(true)
|
||||
const needRefresh = ref(false) // 是否需要刷新
|
||||
|
||||
// 加载列表
|
||||
async function loadList(reset = false) {
|
||||
if (loading.value) return
|
||||
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
list.value = []
|
||||
}
|
||||
|
||||
if (!hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
status: currentStatus.value.value || undefined
|
||||
})
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
const newData = (res.data as any).data
|
||||
list.value = reset ? newData : [...list.value, ...newData]
|
||||
hasMore.value = newData.length >= pageSize.value
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载列表失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 状态改变
|
||||
function onStatusChange(e: any) {
|
||||
const index = e.detail.value
|
||||
currentStatus.value = statusOptions[index]
|
||||
loadList(true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (!loading.value && hasMore.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string) {
|
||||
const item = statusOptions.find(s => s.value === status)
|
||||
return item?.label || '未知'
|
||||
}
|
||||
|
||||
// 跳转到详情
|
||||
function goToDetail(id?: string) {
|
||||
if (!id) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/consumables/temp-request-detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到创建页面
|
||||
function goToCreate() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/consumables/temp-request-create'
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到种类管理
|
||||
function goToCatalog() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/consumables/temp-catalog'
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onLoad(() => {
|
||||
loadList(true)
|
||||
// 监听申请变更事件
|
||||
uni.$on('tempRequest:changed', () => {
|
||||
needRefresh.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (needRefresh.value) {
|
||||
loadList(true)
|
||||
needRefresh.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off('tempRequest:changed')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #fff;
|
||||
color: #4b7aff;
|
||||
border: 2rpx solid #4b7aff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1;
|
||||
padding: 0 32rpx 120rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #f5f5f5;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value.reason {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.value.highlight {
|
||||
color: #4b7aff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
font-size: 26rpx;
|
||||
color: #4b7aff;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
@ -74,11 +74,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import queryService from '@/service/queryService.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import config from '@/config.js'
|
||||
import config, { AssetRole } from '@/config'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import * as staffRole from '@/constant/staffRole.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
@ -87,14 +87,21 @@ import util from '@/utils/util.js'
|
||||
const assetOpen = ref(true)
|
||||
|
||||
// 资产管理宫格项
|
||||
const assetItems = [
|
||||
const assetItems = computed(() => {
|
||||
const role = assetsRole.value || 0
|
||||
return [
|
||||
{ key: 'fixed', text: '固定资产', icon: '/static/icons/fixed-assets.png' },
|
||||
{ key: 'mine', text: '我的借用', icon: '/static/icons/my-loans.png' },
|
||||
{ key: 'scan', text: '扫码查看信息', icon: '/static/icons/scan-to-view-info.png' },
|
||||
{ key: 'inventory', text: '资产盘点', icon: '/static/icons/asset-inventory.png' },
|
||||
{ key: 'plan', text: '易耗品领用计划', icon: '/static/icons/consumables-plan.png' },
|
||||
{ key: 'consumable', text: '易耗品盘点', icon: '/static/icons/consumables-inventory.png' }
|
||||
]
|
||||
{ key: 'consumable', text: '易耗品盘点', icon: '/static/icons/consumables-inventory.png' },
|
||||
{ key: 'lent', text: '出借资产管理', icon: '/static/icons/lent-assets-management.png', show: !!(role & AssetRole.TEAM_LEAD) },
|
||||
{ key: 'request', text: '临时易耗品申请', icon: '/static/icons/consumables-temp-request.png', show: !!(role & AssetRole.TEAM_LEAD) },
|
||||
{ key: 'approve', text: '临时易耗品申请审批', icon: '/static/icons/consumables-temp-approve.png', show: !!(role & AssetRole.CONSUMABLES_MANAGER) },
|
||||
].filter(item => item.show !== false)
|
||||
})
|
||||
|
||||
|
||||
// 轮播图状态
|
||||
const swiperIndex = ref(0)
|
||||
@ -142,6 +149,7 @@ onMounted(() => {
|
||||
// 登录与角色态
|
||||
const isLoggedIn = ref(false)
|
||||
const staff = ref<any | null>(null)
|
||||
const assetsRole = ref<number | null>(null) // 资产系统用户信息
|
||||
const isWeixin = ref(false)
|
||||
const isPatient = ref(true)
|
||||
const isDoctor = ref(false)
|
||||
@ -149,6 +157,7 @@ const isRider = ref(false)
|
||||
|
||||
function syncLoginState() {
|
||||
staff.value = uni.getStorageSync('staff') || null
|
||||
assetsRole.value = uni.getStorageSync('assetsRole') || null // 资产系统用户信息
|
||||
isLoggedIn.value = !!staff.value
|
||||
// 平台
|
||||
// @ts-ignore
|
||||
@ -192,7 +201,7 @@ function toggleAsset() {
|
||||
assetOpen.value = !assetOpen.value
|
||||
}
|
||||
function handleAsset(key: string) {
|
||||
const item = assetItems.find(i => i.key === key)
|
||||
const item = assetItems.value.find((i: any) => i.key === key)
|
||||
if (!item) return
|
||||
if (key === 'fixed') {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/index' })
|
||||
@ -218,6 +227,18 @@ function handleAsset(key: string) {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/scan' })
|
||||
return
|
||||
}
|
||||
if (key === 'lent') {
|
||||
uni.showToast({ title: `打开:${item.text}`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (key === 'request') {
|
||||
uni.navigateTo({ url: '/pages/consumables/temp-request' })
|
||||
return
|
||||
}
|
||||
if (key === 'approve') {
|
||||
uni.navigateTo({ url: '/pages/consumables/temp-approve' })
|
||||
return
|
||||
}
|
||||
// 其他入口可在此对接具体页面
|
||||
uni.showToast({ title: `打开:${item.text}`, icon: 'none' })
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
<view class="card form">
|
||||
<view class="field">
|
||||
<text class="label">用户名/电话</text>
|
||||
<text class="label">手机号</text>
|
||||
<input class="input" v-model="loginRequest.username" placeholder="请输入用户名或电话" :focus="usernameFocus" />
|
||||
</view>
|
||||
<view class="field">
|
||||
@ -25,12 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
// @ts-ignore
|
||||
import md5 from '@/utils/md5.js'
|
||||
// @ts-ignore
|
||||
import util from '@/utils/util.js'
|
||||
// @ts-ignore
|
||||
import config from '@/config.js'
|
||||
import { login } from '../api/user'
|
||||
|
||||
const usernameFocus = ref(true)
|
||||
const loginRequest = reactive({
|
||||
@ -66,7 +61,7 @@ async function onLogin() {
|
||||
// 清空旧身份
|
||||
uni.setStorageSync('staff', null)
|
||||
try {
|
||||
const res = await util.request({
|
||||
/* const res = await util.request({
|
||||
url: config.urls.login,
|
||||
method: 'POST',
|
||||
data: {
|
||||
@ -74,23 +69,26 @@ async function onLogin() {
|
||||
password: md5.hexMD5(loginRequest.username + loginRequest.password),
|
||||
typecode: loginRequest.typecode
|
||||
}
|
||||
})
|
||||
if (res?.data?.code === 0) {
|
||||
const staff = res.data.data.userLoginInfo
|
||||
}) */
|
||||
const res = await login(loginRequest.username, loginRequest.password)
|
||||
if (res.code != 0) {
|
||||
uni.showToast({ title: '登录失败,' + res.msg, icon: 'success', duration: 1000 })
|
||||
return
|
||||
}
|
||||
const staff = res.data.userLoginRequestVO
|
||||
// 兼容旧逻辑,附加 role 别名
|
||||
uni.setStorageSync('staff', { ...staff, role: staff.role_ids })
|
||||
uni.setStorageSync('token', res.data.data.token)
|
||||
uni.setStorageSync('token', res.data.token)
|
||||
uni.showToast({ title: '登录成功', icon: 'success', duration: 1000 })
|
||||
uni.setStorageSync('assetsRole', staff.assetsRole) // 保存用户信息到本地缓存
|
||||
// 跳转到首页(tabBar)
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}, 300)
|
||||
} else {
|
||||
uni.showToast({ title: res?.data?.msg || '登录失败', icon: 'error', duration: 3000 })
|
||||
}
|
||||
} catch (err: any) {
|
||||
// util.request 已有统一错误提示
|
||||
console.warn('login error', err)
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('登录失败', e)
|
||||
const msg = e?.message || '登录失败,请稍后再试'
|
||||
uni.showToast({ title: msg, icon: 'none', duration: 2000 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,32 +1,355 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="empty">
|
||||
<image src="/static/icons/messages.png" class="icon" />
|
||||
<text>暂无消息</text>
|
||||
<!-- 头部标题 -->
|
||||
<view class="header">
|
||||
<text class="title">消息通知</text>
|
||||
<text class="count" v-if="pendingCount > 0">{{ pendingCount }}条待处理</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view scroll-y class="content" @scrolltolower="loadMore">
|
||||
<view v-if="list.length === 0 && !loading" class="empty">
|
||||
<image src="/static/icons/messages.png" class="icon" />
|
||||
<text class="empty-text">暂无消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 待审批申请列表 -->
|
||||
<view v-for="item in list" :key="item.id" class="message-card" @tap="goToApprove(item.id)">
|
||||
<view class="card-header">
|
||||
<view class="type-badge">
|
||||
<text class="badge-icon">📋</text>
|
||||
<text class="badge-text">待审批</text>
|
||||
</view>
|
||||
<text class="time">{{ formatTime(item.createdAt) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<text class="message-title">临时易耗品申请</text>
|
||||
<view class="message-content">
|
||||
<view class="content-row">
|
||||
<text class="label">申请人:</text>
|
||||
<text class="value">{{ item.applicantName || '未填写' }}</text>
|
||||
</view>
|
||||
<view class="content-row">
|
||||
<text class="label">部门:</text>
|
||||
<text class="value">{{ item.department || '-' }}</text>
|
||||
</view>
|
||||
<view class="content-row">
|
||||
<text class="label">物料:</text>
|
||||
<text class="value highlight">{{ item.items?.length || 0 }} 项</text>
|
||||
</view>
|
||||
<view class="content-row" v-if="item.reason">
|
||||
<text class="label">原因:</text>
|
||||
<text class="value reason">{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="action-text">点击查看详情 →</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && list.length > 0" class="no-more">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import consumableTempAPI, { type ConsumableRequest } from '@/pages/api/consumable-temp'
|
||||
import { getPendingCount, refreshNow } from '@/utils/messageManager'
|
||||
|
||||
const list = ref<ConsumableRequest[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const hasMore = ref(true)
|
||||
const pendingCount = ref(0)
|
||||
const needRefresh = ref(false) // 是否需要刷新
|
||||
|
||||
// 加载待审批列表
|
||||
async function loadList(reset = false) {
|
||||
if (loading.value) return
|
||||
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
list.value = []
|
||||
}
|
||||
|
||||
if (!hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
status: 'PENDING' // 只显示待审批的
|
||||
})
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
const newData = (res.data as any).data
|
||||
list.value = reset ? newData : [...list.value, ...newData]
|
||||
hasMore.value = newData.length >= pageSize.value
|
||||
|
||||
// 更新待审批数量
|
||||
pendingCount.value = getPendingCount()
|
||||
} else {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载消息失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (!loading.value && hasMore.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
// 一分钟内
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
}
|
||||
// 一小时内
|
||||
if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
}
|
||||
// 一天内
|
||||
if (diff < 86400000) {
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
}
|
||||
// 超过一天
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 跳转到审批详情
|
||||
function goToApprove(id?: string) {
|
||||
if (!id) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/consumables/temp-approve-detail?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onLoad(() => {
|
||||
// 刷新消息数量
|
||||
refreshNow().then(() => {
|
||||
pendingCount.value = getPendingCount()
|
||||
})
|
||||
|
||||
// 加载列表
|
||||
loadList(true)
|
||||
|
||||
// 监听消息变更事件
|
||||
uni.$on('tempMessage:changed', () => {
|
||||
needRefresh.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// 每次显示时都刷新消息数量
|
||||
pendingCount.value = getPendingCount()
|
||||
|
||||
if (needRefresh.value) {
|
||||
loadList(true)
|
||||
needRefresh.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off('tempMessage:changed')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
background: #f5f7fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 26rpx;
|
||||
color: #f57c00;
|
||||
background: #fff3e0;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.message-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background: #fff3e0;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 24rpx;
|
||||
color: #f57c00;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.content-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value.highlight {
|
||||
color: #3a5ddd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value.reason {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
color: #3a5ddd;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -10,7 +10,12 @@
|
||||
<view class="info" v-else>
|
||||
<text class="name">{{ staff?.user_name || staff?.name || '已登录' }}</text>
|
||||
<text class="sub">类型:{{ userTypeText }}</text>
|
||||
<text class="sub">角色:{{ staff?.role_name || '-' }}</text>
|
||||
<!-- <text class="sub">角色:{{ staff?.role_name || '-' }}</text> -->
|
||||
<text class="sub">角色:{{ [
|
||||
assetRole & AssetRole.CONSUMABLES_MANAGER ? '易耗品发放负责人' : undefined,
|
||||
assetRole & AssetRole.DEVICE_MANAGER ? '设备管理人员' : undefined,
|
||||
assetRole & AssetRole.TEAM_LEAD ? '小组负责人' : undefined
|
||||
].filter(Boolean).join('、') || '普通用户'}}</text>
|
||||
<text class="sub" v-if="staff?.job_no">工号:{{ staff?.job_no }}</text>
|
||||
<view class="actions">
|
||||
<button class="btn btn-danger" @tap="logout">退出登录</button>
|
||||
@ -19,12 +24,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 我的任务标题 -->
|
||||
<view class="section-title">
|
||||
<!-- <view class="section-title">
|
||||
<text>我的任务</text>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<view class="task-list">
|
||||
<!-- <view class="task-list">
|
||||
<view v-if="loading" class="empty">加载中...</view>
|
||||
<view v-else-if="!taskList.length" class="empty">暂无任务</view>
|
||||
<view v-else class="list-wrap">
|
||||
@ -92,10 +97,10 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 详情弹层 -->
|
||||
<view v-if="isDispDetail" class="modal">
|
||||
<!-- <view v-if="isDispDetail" class="modal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">当前运单信息</text>
|
||||
@ -127,10 +132,10 @@
|
||||
<button class="btn" @tap="isDispDetail = false">关闭</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 评价弹层 -->
|
||||
<view v-if="isEvaluate" class="modal">
|
||||
<!-- <view v-if="isEvaluate" class="modal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">请对担架运送服务进行评价</text>
|
||||
@ -156,7 +161,7 @@
|
||||
<button class="btn" @tap="cancelEvaluate">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</scroll-view>
|
||||
|
||||
</template>
|
||||
@ -169,12 +174,13 @@ import queryService from '@/service/queryService.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import getService from '@/service/getService.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import config from '@/config.js'
|
||||
import config, { AssetRole } from '@/config'
|
||||
|
||||
type Task = Record<string, any>
|
||||
|
||||
const isLoggedIn = ref(false)
|
||||
const staff = ref<any | null>(null)
|
||||
const assetRole = ref<any | null>(null)
|
||||
const isWeixin = ref(false)
|
||||
const taskList = ref<Task[]>([])
|
||||
const loading = ref(false)
|
||||
@ -228,7 +234,7 @@ async function initEnv() {
|
||||
// 确保 openid
|
||||
const openid = uni.getStorageSync('openid')
|
||||
if (!openid) {
|
||||
try { await getService.getOpenId?.() } catch {}
|
||||
try { await getService.getOpenId?.() } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -321,8 +327,9 @@ function cancelEvaluate() {
|
||||
}
|
||||
|
||||
function sync() {
|
||||
staff.value = uni.getStorageSync('staff') || null
|
||||
isLoggedIn.value = !!staff.value
|
||||
staff.value = uni.getStorageSync('staff') ?? null
|
||||
assetRole.value = uni.getStorageSync('assetsRole') ?? null
|
||||
isLoggedIn.value = !!staff.value || typeof assetRole.value === 'number'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -342,11 +349,13 @@ onShow(async () => {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.profile {
|
||||
margin: 32rpx;
|
||||
padding: 32rpx;
|
||||
@ -354,47 +363,211 @@ onShow(async () => {
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.info { display: flex; flex-direction: column; }
|
||||
.name { font-size: 32rpx; color: #333; }
|
||||
.sub { font-size: 24rpx; color: #999; }
|
||||
.actions { margin-top: 16rpx; }
|
||||
.btn { padding: 12rpx 20rpx; background: #3a5ddd; color: #fff; border-radius: 10rpx; font-size: 24rpx; }
|
||||
.btn-danger { background: #ef5350; }
|
||||
.btn-primary { background: #3a5ddd; }
|
||||
.btn-text { background: transparent; color: #3a5ddd; padding: 6rpx 10rpx; }
|
||||
.btn-text.red { color: #ef5350; }
|
||||
.btn-text.blue { color: #3a5ddd; }
|
||||
.btn-text.yellow { color: #f39c12; }
|
||||
|
||||
.section-title { padding: 0 32rpx 12rpx; color: #666; font-size: 26rpx; }
|
||||
.task-list { padding: 0 32rpx 32rpx; }
|
||||
.empty { text-align: center; color: #9a9aa0; padding: 40rpx 0; }
|
||||
.list-wrap { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.task { padding: 20rpx; }
|
||||
.row { display: flex; align-items: center; padding: 8rpx 0; flex-wrap: wrap; }
|
||||
.two-cols { justify-content: space-between; }
|
||||
.two-cols .col { width: 48%; display: flex; }
|
||||
.label { color: #666; font-size: 24rpx; }
|
||||
.value { color: #333; font-size: 26rpx; }
|
||||
.actions-inline { display: flex; justify-content: flex-end; gap: 16rpx; padding-top: 8rpx; }
|
||||
.actions-inline .btn{ flex: 1;}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12rpx 20rpx;
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef5350;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a5ddd;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: transparent;
|
||||
color: #3a5ddd;
|
||||
padding: 6rpx 10rpx;
|
||||
}
|
||||
|
||||
.btn-text.red {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.btn-text.blue {
|
||||
color: #3a5ddd;
|
||||
}
|
||||
|
||||
.btn-text.yellow {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 32rpx 12rpx;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9a9aa0;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.task {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.two-cols {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.two-cols .col {
|
||||
width: 48%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.actions-inline {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.actions-inline .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 简易弹层样式 */
|
||||
.modal { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
||||
.modal-content { width: 86%; max-height: 76vh; background: #fff; border-radius: 16rpx; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.modal-header { padding: 20rpx 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #eee; }
|
||||
.modal-title { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.modal-close { font-size: 40rpx; color: #999; }
|
||||
.modal-body { padding: 16rpx 24rpx; max-height: 60vh; }
|
||||
.modal-footer { padding: 16rpx 24rpx; display: flex; justify-content: flex-end; gap: 16rpx; border-top: 1rpx solid #eee; }
|
||||
.modal-footer .btn { flex: 1; }
|
||||
.form-row { margin-bottom: 20rpx; }
|
||||
.radio-group { display: flex; gap: 24rpx; flex-wrap: wrap; }
|
||||
.radio-item { display: flex; align-items: center; gap: 8rpx; }
|
||||
.textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #e5e6eb; border-radius: 12rpx; padding: 12rpx; font-size: 26rpx; }
|
||||
.modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 86%;
|
||||
max-height: 76vh;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 16rpx 24rpx;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16rpx 24rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
border: 1rpx solid #e5e6eb;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -138,7 +138,7 @@ import queryService from '@/service/queryService.js'
|
||||
// @ts-ignore
|
||||
import getService from '@/service/getService.js'
|
||||
// @ts-ignore
|
||||
import config from '@/config.js'
|
||||
import config from '@/config'
|
||||
|
||||
const showPayment = ref(true) // 如不需要可置为 false
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ import queryService from '@/service/queryService.js'
|
||||
// @ts-ignore
|
||||
import getService from '@/service/getService.js'
|
||||
// @ts-ignore
|
||||
import config from '@/config.js'
|
||||
import config from '@/config'
|
||||
|
||||
// 表单数据(字段需与旧版保持一致)
|
||||
const form = reactive({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import config from '../config.js';
|
||||
import config from '../config';
|
||||
import util from '../utils/util.js';
|
||||
import queryService from '@/service/queryService.js';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import config from '../config.js';
|
||||
import config from '../config';
|
||||
import util from '../utils/util.js';
|
||||
import * as taskState from '../constant/taskState.js';
|
||||
import taskType from '../constant/taskType.js';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import util from "../utils/util.js";
|
||||
import config from "../config.js";
|
||||
import config from "../config";
|
||||
|
||||
export default {
|
||||
// 上传文件
|
||||
|
||||
BIN
src/static/icons/consumables-temp-approve.png
Normal file
BIN
src/static/icons/consumables-temp-approve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
src/static/icons/consumables-temp-request.png
Normal file
BIN
src/static/icons/consumables-temp-request.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
src/static/icons/lent-assets-management.png
Normal file
BIN
src/static/icons/lent-assets-management.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
103
src/utils/messageManager.ts
Normal file
103
src/utils/messageManager.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 消息管理工具
|
||||
* 用于管理待审批消息数量和 TabBar 小红点
|
||||
*/
|
||||
|
||||
import consumableTempAPI from '@/pages/api/consumable-temp'
|
||||
|
||||
// 消息数量缓存
|
||||
let pendingCount = 0
|
||||
let pollingTimer: number | null = null
|
||||
|
||||
/**
|
||||
* 获取待审批数量
|
||||
*/
|
||||
export async function fetchPendingCount(): Promise<number> {
|
||||
try {
|
||||
const res = await consumableTempAPI.getRequestList({
|
||||
page: 1,
|
||||
pageSize: 100, // 获取足够多的数据来计算总数
|
||||
status: 'PENDING'
|
||||
})
|
||||
|
||||
if (res.statusCode === 200 && (res.data as any)?.data) {
|
||||
const data = (res.data as any).data
|
||||
pendingCount = data.length || 0
|
||||
|
||||
// 更新 TabBar 小红点
|
||||
updateTabBarBadge(pendingCount)
|
||||
|
||||
return pendingCount
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取待审批数量失败:', err)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 TabBar 消息的小红点
|
||||
*/
|
||||
function updateTabBarBadge(count: number) {
|
||||
try {
|
||||
if (count > 0) {
|
||||
// 显示小红点,如果数量大于0则显示数字
|
||||
uni.setTabBarBadge({
|
||||
index: 1, // 消息 tab 的索引(从0开始)
|
||||
text: count > 99 ? '99+' : count.toString()
|
||||
})
|
||||
} else {
|
||||
// 移除小红点
|
||||
uni.removeTabBarBadge({
|
||||
index: 1
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
// 某些页面没有 TabBar,忽略错误
|
||||
console.warn('更新 TabBar 角标失败(可能当前页面无 TabBar):', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前待审批数量(从缓存)
|
||||
*/
|
||||
export function getPendingCount(): number {
|
||||
return pendingCount
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始定时轮询
|
||||
* @param interval 轮询间隔(毫秒),默认 30 秒
|
||||
*/
|
||||
export function startPolling(interval: number = 30000) {
|
||||
// 先立即执行一次
|
||||
fetchPendingCount()
|
||||
|
||||
// 清除之前的定时器
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
pollingTimer = setInterval(() => {
|
||||
fetchPendingCount()
|
||||
}, interval) as unknown as number
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时轮询
|
||||
*/
|
||||
export function stopPolling() {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
pollingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动刷新(用于审批操作后立即更新)
|
||||
*/
|
||||
export function refreshNow() {
|
||||
return fetchPendingCount()
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import config from '../config.js';
|
||||
import config from '../config';
|
||||
|
||||
const getStateCreateTime = (stateList, type) => {
|
||||
if(stateList && stateList.length != 0){
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user