Files
smartDriveEEUniApp/pages-subpackage/furniture_reception/sop_management_my.vue
2026-04-23 16:27:48 +08:00

710 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<view class="toolbar">
<text class="toolbar-total">{{ total }}</text>
<input
class="toolbar-input"
v-model="query.title"
placeholder="SOP标题"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<input
class="toolbar-input"
v-model="query.category"
placeholder="分类"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<input
class="toolbar-input"
v-model="query.status"
placeholder="状态"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--add" @click="openCreateDialog">
<uni-icons type="plusempty" size="16" color="#16a34a" />
<text>新增</text>
</view>
<view class="toolbar-btn toolbar-btn--refresh" @click="onRefresh">
<uni-icons type="refresh" size="16" color="#2A68FF" />
<text>刷新</text>
</view>
</view>
</view>
<scroll-view class="list" scroll-y enable-flex @scrolltolower="onReachBottom">
<view class="card" v-for="item in list" :key="item.id">
<view class="card-header">
<view class="title-wrap">
<text class="title">{{ item.title || '未命名SOP' }}</text>
<text v-if="item.status" class="status">{{ item.status }}</text>
</view>
<view class="top-flag" v-if="item.isTop">置顶</view>
</view>
<view class="meta">
<text>分类{{ item.category || '-' }}</text>
<text>行业{{ item.industry || '-' }}</text>
<text>创建人{{ item.creator || '-' }}</text>
</view>
<view class="content-line" v-if="item.content">内容{{ item.content }}</view>
<view class="meta">
<text>浏览{{ item.viewCount || 0 }}</text>
<text>点赞{{ item.likeCount || 0 }}</text>
<text>排序{{ item.sortOrder || 0 }}</text>
</view>
<view class="content-line" v-if="item.tags">标签{{ item.tags }}</view>
<view class="time-line">
<text>创建{{ formatDateTime(item.createTime) || '-' }}</text>
<text>更新{{ formatDateTime(item.updateTime) || '-' }}</text>
</view>
<view class="actions">
<view class="action-btn action-btn--edit" @click="openEditDialog(item)">编辑</view>
<view class="action-btn action-btn--delete" @click="onDelete(item)">删除</view>
</view>
</view>
<view class="empty" v-if="!loading && !list.length">
<text>暂无SOP数据</text>
</view>
<view class="loading-more" v-if="loading && list.length">
<text>正在加载...</text>
</view>
</scroll-view>
<view class="dialog-mask" v-if="showDialog" @click="closeDialog">
<view class="dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">{{ form.id ? '编辑SOP' : '新增SOP' }}</text>
<view class="dialog-close" @click="closeDialog">
<uni-icons type="close" size="18" color="#999" />
</view>
</view>
<scroll-view class="dialog-body" scroll-y>
<view class="form-item">
<text class="form-label">SOP标题</text>
<input class="form-input" v-model="form.title" placeholder="请输入SOP标题" />
</view>
<view class="form-item">
<text class="form-label">SOP分类</text>
<input class="form-input" v-model="form.category" placeholder="请输入SOP分类" />
</view>
<view class="form-item">
<text class="form-label">应用行业</text>
<input class="form-input" v-model="form.industry" placeholder="请输入应用行业" />
</view>
<view class="form-item">
<text class="form-label">状态</text>
<input class="form-input" v-model="form.status" placeholder="如:已发布/草稿/已下架" />
</view>
<view class="form-item">
<text class="form-label">SOP内容</text>
<textarea class="form-textarea" v-model="form.content" placeholder="请输入SOP内容" />
</view>
<view class="form-item">
<text class="form-label">标签逗号分隔</text>
<input class="form-input" v-model="form.tags" placeholder="如:流程,接待,标准化" />
</view>
<view class="form-item">
<text class="form-label">附件URL</text>
<input class="form-input" v-model="form.attachmentUrl" placeholder="请输入附件URL" />
</view>
<view class="form-item">
<text class="form-label">排序权重</text>
<input class="form-input" v-model="form.sortOrder" type="number" placeholder="请输入排序权重" />
</view>
<view class="form-item row-item">
<text class="form-label">是否置顶</text>
<switch :checked="!!form.isTop" @change="onTopSwitchChange" color="#2A68FF" />
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注" />
</view>
</scroll-view>
<view class="dialog-footer">
<view class="dialog-btn dialog-btn--cancel" @click="closeDialog">取消</view>
<view class="dialog-btn dialog-btn--confirm" @click="onSubmit">保存</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getApiUrl } from "@/common/config.js";
function createDefaultForm() {
return {
id: "",
title: "",
category: "",
industry: "",
status: "",
content: "",
tags: "",
attachmentUrl: "",
sortOrder: "",
isTop: false,
remark: "",
};
}
export default {
data() {
return {
loading: false,
total: 0,
page: {
current: 1,
size: 10,
},
query: {
title: "",
category: "",
industry: "",
creator: "",
status: "",
},
list: [],
showDialog: false,
form: createDefaultForm(),
};
},
mounted() {
this.fetchList({ force: true });
},
methods: {
getAuthHeaders() {
let tenantId = "";
let token = "";
try {
tenantId = uni.getStorageSync("backend-tenant-id") || "";
token = uni.getStorageSync("backend-token") || "";
} catch (e) {
console.error("读取认证信息失败:", e);
}
const headers = {
"Content-Type": "application/json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
if (tenantId) {
headers["X-Tenant-Id"] = tenantId;
}
return headers;
},
buildQueryString() {
const payload = {
current: this.page.current,
size: this.page.size,
title: this.query.title.trim(),
category: this.query.category.trim(),
industry: this.query.industry.trim(),
creator: this.query.creator.trim(),
status: this.query.status.trim(),
};
return Object.keys(payload)
.filter((k) => payload[k] !== undefined && payload[k] !== null && payload[k] !== "")
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(payload[k])}`)
.join("&");
},
onSearch() {
this.page.current = 1;
this.fetchList({ force: true });
},
onRefresh() {
this.page.current = 1;
this.fetchList({ force: true });
},
onReachBottom() {
if (this.loading) return;
if (this.list.length >= this.total) return;
this.page.current += 1;
this.fetchList();
},
async fetchList({ force = false } = {}) {
if (this.loading && !force) return;
this.loading = true;
try {
const qs = this.buildQueryString();
const baseUrl = getApiUrl("/api/sopManagementMy/list");
const url = qs ? `${baseUrl}?${qs}` : baseUrl;
const res = await uni.request({
url,
method: "GET",
header: this.getAuthHeaders(),
timeout: 30000,
});
if (res.statusCode === 200 && res.data?.success) {
const rawData = res.data?.data;
const records = Array.isArray(rawData)
? rawData
: Array.isArray(rawData?.records)
? rawData.records
: [];
const total = Number(res.data?.total ?? rawData?.total ?? records.length) || 0;
this.total = total;
if (this.page.current === 1 || force) {
this.list = [...records];
} else {
this.list = [...this.list, ...records];
}
return;
}
this.handleRequestError(res.data?.message || "获取SOP列表失败");
} catch (error) {
console.error("获取SOP列表失败:", error);
this.handleRequestError("获取SOP列表失败请稍后重试");
} finally {
this.loading = false;
}
},
openCreateDialog() {
this.form = createDefaultForm();
this.showDialog = true;
},
openEditDialog(item) {
this.form = {
id: item.id || "",
title: item.title || "",
category: item.category || "",
industry: item.industry || "",
status: item.status || "",
content: item.content || "",
tags: item.tags || "",
attachmentUrl: item.attachmentUrl || "",
sortOrder: item.sortOrder === undefined || item.sortOrder === null ? "" : String(item.sortOrder),
isTop: !!item.isTop,
remark: item.remark || "",
};
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
onTopSwitchChange(e) {
this.form.isTop = !!e?.detail?.value;
},
async onSubmit() {
const title = this.form.title.trim();
if (!title) {
uni.showToast({ title: "请输入SOP标题", icon: "none" });
return;
}
const sortOrderNumber =
this.form.sortOrder === "" || this.form.sortOrder === null || this.form.sortOrder === undefined
? undefined
: Number(this.form.sortOrder);
const payload = {
id: this.form.id || undefined,
title,
category: this.form.category.trim(),
industry: this.form.industry.trim(),
status: this.form.status.trim(),
content: this.form.content.trim(),
tags: this.form.tags.trim(),
attachmentUrl: this.form.attachmentUrl.trim(),
sortOrder: Number.isFinite(sortOrderNumber) ? sortOrderNumber : undefined,
isTop: !!this.form.isTop,
remark: this.form.remark.trim(),
};
const isEdit = !!this.form.id;
const url = getApiUrl(isEdit ? "/api/sopManagementMy/update" : "/api/sopManagementMy/add");
const method = isEdit ? "PUT" : "POST";
try {
uni.showLoading({ title: "保存中..." });
const res = await uni.request({
url,
method,
data: payload,
header: this.getAuthHeaders(),
timeout: 30000,
});
uni.hideLoading();
if (res.statusCode === 200 && res.data?.success) {
uni.showToast({ title: isEdit ? "更新成功" : "新增成功", icon: "success" });
this.closeDialog();
this.page.current = 1;
this.fetchList({ force: true });
return;
}
this.handleRequestError(res.data?.message || "保存失败");
} catch (error) {
uni.hideLoading();
console.error("保存SOP失败:", error);
this.handleRequestError("保存失败,请稍后重试");
}
},
onDelete(item) {
if (!item?.id) return;
uni.showModal({
title: "确认删除",
content: `确定删除「${item.title || "该SOP"}」吗?`,
success: async (res) => {
if (!res.confirm) return;
try {
uni.showLoading({ title: "删除中..." });
const url = getApiUrl(`/api/sopManagementMy/delete/${encodeURIComponent(item.id)}`);
const delRes = await uni.request({
url,
method: "DELETE",
header: this.getAuthHeaders(),
timeout: 30000,
});
uni.hideLoading();
if (delRes.statusCode === 200 && delRes.data?.success) {
uni.showToast({ title: "删除成功", icon: "success" });
this.page.current = 1;
this.fetchList({ force: true });
return;
}
this.handleRequestError(delRes.data?.message || "删除失败");
} catch (error) {
uni.hideLoading();
console.error("删除SOP失败:", error);
this.handleRequestError("删除失败,请稍后重试");
}
},
});
},
handleRequestError(message) {
this.list = this.page.current === 1 ? [] : this.list;
this.total = this.page.current === 1 ? 0 : this.total;
uni.showToast({
title: message || "请求失败",
icon: "none",
});
},
formatDateTime(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hour = String(date.getHours()).padStart(2, "0");
const minute = String(date.getMinutes()).padStart(2, "0");
const second = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
},
};
</script>
<style scoped>
.page {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
padding: 0 16rpx;
box-sizing: border-box;
}
.toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8rpx;
padding: 12rpx 8rpx;
margin: 16rpx 0 24rpx;
background-color: #f8f8fa;
border-radius: 8rpx;
border: 1px solid #efeff2;
overflow-x: auto;
min-height: 64rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.toolbar-total {
font-size: 24rpx;
color: #666;
white-space: nowrap;
flex-shrink: 0;
margin-right: 8rpx;
}
.toolbar-input {
flex: 1;
min-width: 120rpx;
max-width: 200rpx;
height: 56rpx;
line-height: 56rpx;
background-color: #f5f6fa;
border-radius: 8rpx;
padding: 0 12rpx;
font-size: 24rpx;
border: 1px solid transparent;
box-sizing: border-box;
}
.toolbar-actions {
display: flex;
align-items: center;
margin-left: auto;
gap: 8rpx;
flex-shrink: 0;
}
.toolbar-btn {
height: 56rpx;
min-width: 56rpx;
padding: 0 12rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
font-size: 24rpx;
}
.toolbar-btn--add {
color: #16a34a;
}
.toolbar-btn--refresh {
color: #2a68ff;
}
.list {
flex: 1;
box-sizing: border-box;
}
.card {
width: 100%;
background-color: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14rpx;
}
.title-wrap {
display: flex;
align-items: center;
gap: 10rpx;
min-width: 0;
}
.title {
font-size: 30rpx;
color: #111827;
font-weight: 600;
max-width: 420rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
font-size: 22rpx;
color: #2563eb;
padding: 4rpx 10rpx;
border-radius: 999rpx;
background: #eaf2ff;
}
.top-flag {
font-size: 22rpx;
color: #b45309;
background-color: #fff7ed;
padding: 4rpx 10rpx;
border-radius: 999rpx;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 10rpx 16rpx;
margin-bottom: 12rpx;
}
.meta text,
.content-line,
.time-line text {
font-size: 24rpx;
color: #4b5563;
}
.content-line {
margin-bottom: 10rpx;
line-height: 1.5;
}
.time-line {
display: flex;
flex-wrap: wrap;
gap: 8rpx 16rpx;
padding-top: 12rpx;
border-top: 1px solid #f3f4f6;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12rpx;
margin-top: 16rpx;
}
.action-btn {
font-size: 24rpx;
padding: 8rpx 18rpx;
border-radius: 10rpx;
border: 1px solid transparent;
}
.action-btn--edit {
color: #2563eb;
border-color: rgba(37, 99, 235, 0.3);
background-color: #f0f6ff;
}
.action-btn--delete {
color: #dc2626;
border-color: rgba(220, 38, 38, 0.28);
background-color: #fff4f4;
}
.empty,
.loading-more {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 40rpx 0;
}
.dialog-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
width: 660rpx;
max-height: 84vh;
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 30rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
}
.dialog-title {
font-size: 32rpx;
color: #111827;
font-weight: 500;
}
.dialog-close {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-body {
flex: 1;
max-height: 900rpx;
padding: 24rpx 30rpx;
box-sizing: border-box;
}
.form-item {
margin-bottom: 22rpx;
}
.row-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-label {
display: block;
font-size: 26rpx;
color: #374151;
margin-bottom: 10rpx;
}
.form-input,
.form-textarea {
width: 100%;
background-color: #f9fafb;
border-radius: 10rpx;
box-sizing: border-box;
font-size: 26rpx;
color: #111827;
}
.form-input {
height: 80rpx;
padding: 0 20rpx;
}
.form-textarea {
min-height: 140rpx;
padding: 16rpx 20rpx;
line-height: 1.5;
}
.dialog-footer {
display: flex;
gap: 18rpx;
padding: 18rpx 30rpx 26rpx;
border-top: 1px solid #f0f0f0;
}
.dialog-btn {
flex: 1;
height: 82rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
}
.dialog-btn--cancel {
background-color: #f3f4f6;
color: #6b7280;
}
.dialog-btn--confirm {
background-color: #4c8dff;
color: #fff;
}
</style>