feat: app通知附件显示加载

property-only-app
wx-jincw 2 months ago
parent 0176a9d2c0
commit 28d9b65d27

@ -1,8 +1,34 @@
{
"name": "app",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "app",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"json-bigint": "^1.0.0"
}
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"engines": {
"node": "*"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
}
},
"dependencies": {
"bignumber.js": {
"version": "9.3.1",

@ -79,6 +79,27 @@
<text class="count">{{ form.csContent.length }}/300</text>
</view>
<view class="form-item">
<text class="label">上传图片选填</text>
<view class="upload-section">
<view class="upload-list">
<view
class="upload-item"
v-for="(image, index) in images"
:key="index"
>
<image :src="HTTP_ADMIN_URL + '/' + image.url" mode="aspectFill"></image>
<text class="delete-btn" @click="deleteImage(index)">×</text>
</view>
<view class="upload-btn" @click="chooseImage" v-if="images.length < 9">
<text class="iconfont icon-tianjia"></text>
<text>添加图片</text>
</view>
</view>
<text class="hint">最多上传9张图片</text>
</view>
</view>
<view class="form-item remark-item">
<text class="label">备注选填</text>
<input
@ -153,6 +174,22 @@
{{ item.handlerDate }}
</text>
</view>
<view
class="image-grid"
v-if="item.files && item.files.length"
>
<view
class="image-item"
v-for="(file, imgIndex) in item.files"
:key="imgIndex"
@click="previewRecordImages(item.files, imgIndex)"
>
<image
:src="HTTP_ADMIN_URL + '/' + (file.url || file.attDir)"
mode="aspectFill"
></image>
</view>
</view>
</view>
</view>
@ -182,11 +219,14 @@ import {
listComplaintSuggestion,
deleteComplaintSuggestion
} from '@/api/property.js';
import request from '@/utils/request.js';
import { HTTP_ADMIN_URL } from '@/config/app';
export default {
dicts: ['cs_type'],
data() {
return {
HTTP_ADMIN_URL,
activeTab: 'form',
form: {
csType: '',
@ -196,6 +236,7 @@ export default {
phone: '',
},
typeIndex: 0,
images: [],
records: [],
page: 1,
limit: 10,
@ -239,6 +280,61 @@ export default {
this.form.csType = types[index].dictValue;
}
},
chooseImage() {
// #ifndef MP-WEIXIN
uni.chooseImage({
count: 9 - this.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.uploadImages(res.tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
// #ifdef MP-WEIXIN
uni.chooseMedia({
count: 9 - this.images.length,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: (res) => {
const tempFilePaths = res.tempFiles.map(file => file.tempFilePath);
this.uploadImages(tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
},
async uploadImages(filePaths) {
for (let i = 0; i < filePaths.length; i++) {
try {
uni.showLoading({ title: '上传中...', mask: true });
const result = await request.uploadFile(filePaths[i], 'multipart', {}, {
params: {
model: 'app-complaint',
pid: 10
}
});
this.images.push(result);
} catch (e) {
console.error('上传图片失败:', e);
uni.showToast({
title: '上传失败:' + (typeof e === 'string' ? e : '未知错误'),
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
},
deleteImage(index) {
this.images.splice(index, 1);
},
async submitComplaint() {
if (!this.form.csContent.trim()) {
uni.showToast({
@ -248,12 +344,28 @@ export default {
return;
}
const imageFiles = this.images.map(file => {
let attDir = file.url;
return {
attId: file.id + '',
name: file.fileName,
attDir,
attSize: file.fileSize,
attType: file.type,
fileName: file.fileName,
filePath: attDir,
url: attDir,
originalFileName: file.fileName
};
});
const payload = {
csType: this.form.csType,
submitChannel: this.form.submitChannel,
csContent: this.form.csContent.trim(),
remark: this.form.remark.trim(),
phone: this.form.phone.trim(),
files: imageFiles
};
try {
@ -267,6 +379,7 @@ export default {
this.form.csContent = '';
this.form.remark = '';
this.images = [];
this.page = 1;
this.records = [];
@ -327,6 +440,17 @@ export default {
if (status === '3') return '已处理';
return status;
},
previewRecordImages(files, index) {
if (!files || !files.length) return;
const urls = files.map(file => {
const path = file.url || file.attDir || file.filePath;
return this.HTTP_ADMIN_URL + '/' + path;
});
uni.previewImage({
current: index,
urls
});
},
async handleDelete(id) {
uni.showModal({
title: '确认删除',
@ -477,6 +601,74 @@ export default {
color: #333;
box-sizing: border-box;
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.upload-item {
width: 180rpx;
height: 180rpx;
position: relative;
border-radius: 10rpx;
overflow: hidden;
border: 1rpx solid #e5e5e5;
image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 36rpx;
height: 36rpx;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
line-height: 1;
}
}
.upload-btn {
width: 180rpx;
height: 180rpx;
border: 1rpx dashed #d9d9d9;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fafafa;
.iconfont {
font-size: 40rpx;
color: #bfbfbf;
margin-bottom: 10rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
}
.hint {
display: block;
margin-top: 16rpx;
font-size: 20rpx;
color: #999;
}
}
}
.remark-item {
@ -590,6 +782,28 @@ export default {
color: #333;
}
}
.image-grid {
margin-top: 12rpx;
margin-left: 0;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.image-item {
width: calc((100% - 24rpx) / 3);
aspect-ratio: 1 / 1;
border-radius: 8rpx;
overflow: hidden;
background-color: #f5f5f5;
image {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}

@ -42,20 +42,9 @@
<text class="type line1">{{ item.noticeTitle || '未命名通知' }}</text>
</view>
<view class="tag-row">
<text class="tag kind" :class="typeClassMap[item.type] || ''">
{{ typeText(item.type) }}
</text>
<text class="tag status" :class="statusClassMap[item.status] || statusClassMap['__default__']">
{{ statusText(item.status) }}
</text>
<text class="tag sms" :class="smsClassMap[item.smsStatus] || ''">
{{ smsText(item.smsStatus) }}
</text>
</view>
</view>
<text class="time">{{ formatDate(item.noticeTime) }}</text>
<text class="time">{{ item.noticeTime }}</text>
</view>
<view class="card-body">
@ -68,12 +57,8 @@
<text class="value">{{ item.noticeScope || '—' }}</text>
</view>
<view class="row">
<text class="label">拟稿单位</text>
<text class="value">{{ item.draftDept || '—' }}</text>
</view>
<view class="row" v-if="item.noticeSource">
<text class="label">通知来源</text>
<text class="value">{{ item.noticeSource }}</text>
<text class="label">通知单位</text>
<text class="value">{{ item.draftDeptName || '—' }}</text>
</view>
<view class="preview" v-if="item.noticeContent">
@ -97,6 +82,7 @@
import { pubnoticeListApi } from '@/api/pubnotice.js';
export default {
dicts: ['sys_notice_type'],
data() {
return {
query: {
@ -109,21 +95,7 @@ export default {
limit: 10,
loading: false,
finished: false,
typeClassMap: {
1: 'tag-internal',
2: 'tag-external',
'__default__': ''
},
statusClassMap: {
0: 'status-draft',
1: 'status-submit',
'__default__': ''
},
smsClassMap: {
0: 'sms-unsent',
1: 'sms-sent',
'__default__': ''
}
};
},
onLoad() {
@ -202,38 +174,14 @@ export default {
},
contentPreview(content) {
if (content === undefined || content === null) return '';
const s = String(content).replace(/\s+/g, ' ').trim();
const s = String(content)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!s) return '';
return s.length > 80 ? s.slice(0, 80) + '...' : s;
},
formatDate(val) {
if (!val) return '';
if (typeof val === 'string') {
return val.replace('T', ' ').slice(0, 19);
}
if (val instanceof Date) {
const pad = (n) => (n < 10 ? '0' + n : '' + n);
return `${val.getFullYear()}-${pad(val.getMonth() + 1)}-${pad(val.getDate())} ${pad(
val.getHours()
)}:${pad(val.getMinutes())}`;
}
return String(val);
},
typeText(type) {
if (type === 1 || type === '1') return '内部';
if (type === 2 || type === '2') return '外部';
return type || '—';
},
statusText(status) {
if (status === 0 || status === '0') return '草稿';
if (status === 1 || status === '1') return '提交';
return status ?? '—';
},
smsText(smsStatus) {
if (smsStatus === 0 || smsStatus === '0') return '未发';
if (smsStatus === 1 || smsStatus === '1') return '已发';
return smsStatus ?? '—';
},
},
};
</script>
@ -353,49 +301,6 @@ export default {
}
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
line-height: 1;
}
.tag-internal {
background-color: #e6f7ff;
color: #409eff;
}
.tag-external {
background-color: #fff7e6;
color: #fa8c16;
}
.status-draft {
background-color: #f5f5f5;
color: #666;
}
.status-submit {
background-color: #f6ffed;
color: #52c41a;
}
.sms-unsent {
background-color: #f5f5f5;
color: #999;
}
.sms-sent {
background-color: #e6fffb;
color: #13c2c2;
}
.card-body {
.row {
display: flex;

@ -5,11 +5,6 @@
<view class="title-section">
<text class="detail-title">{{ notice.noticeTitle || '未命名通知' }}</text>
<view class="tag-row">
<text class="tag kind" :class="typeClassMap[notice.type] || ''">{{ typeText(notice.type) }}</text>
<text class="tag status" :class="statusClassMap[notice.status] || statusClassMap['__default__']">{{ statusText(notice.status) }}</text>
<text class="tag sms" :class="smsClassMap[notice.smsStatus] || ''">{{ smsText(notice.smsStatus) }}</text>
</view>
</view>
<view class="info-list">
@ -17,29 +12,17 @@
<text class="label">通知类型</text>
<text class="value">{{ notice.noticeType }}</text>
</view>
<view class="row" v-if="notice.noticeSource">
<text class="label">通知来源</text>
<text class="value">{{ notice.noticeSource }}</text>
</view>
<view class="row" v-if="notice.draftDept">
<text class="label">拟稿单位</text>
<text class="value">{{ notice.draftDept }}</text>
<view class="row" v-if="notice.draftDeptName">
<text class="label">通知单位</text>
<text class="value">{{ notice.draftDeptName }}</text>
</view>
<view class="row" v-if="notice.noticeScope">
<text class="label">通知范围</text>
<text class="value">{{ notice.noticeScope }}</text>
</view>
<view class="row" v-if="notice.bizType || notice.bizId">
<text class="label">业务信息</text>
<text class="value">{{ bizText(notice.bizType, notice.bizId) }}</text>
</view>
<view class="row" v-if="notice.noticeTime">
<text class="label">通知时间</text>
<text class="value">{{ formatDate(notice.noticeTime) }}</text>
</view>
<view class="row" v-if="notice.remark">
<text class="label">备注</text>
<text class="value remark-value">{{ notice.remark }}</text>
<text class="value">{{ notice.noticeTime }}</text>
</view>
</view>
@ -52,6 +35,36 @@
></jyf-parser>
<view class="empty" v-else></view>
</view>
<view class="attach-box" v-if="fileList.length">
<view class="attach-title">附件列表</view>
<view class="attach-list">
<view
class="attach-item"
v-for="(file, index) in fileList"
:key="file.attId || file.fileId || index"
@click="fileClick(file)"
>
<view class="left">
<image
v-if="isImageFile(file)"
class="thumb"
:src="getFileUrl(file)"
mode="aspectFill"
></image>
<view v-else class="file-icon"></view>
<view class="meta">
<text class="name">{{ file.name || '未命名附件' }}</text>
<text class="desc">
{{ (file.attType || '文件').toUpperCase() }}
<text v-if="formatFileSize(file.attSize)"> · {{ formatFileSize(file.attSize) }}</text>
</text>
</view>
</view>
<text class="preview-btn">{{ isImageFile(file) ? '预览' : '打开' }}</text>
</view>
</view>
</view>
</view>
<view class="empty-page" v-else>
@ -69,13 +82,16 @@
<script>
import jyfParser from '@/components/jyf-parser/jyf-parser';
import { pubnoticeDetailApi } from '@/api/pubnotice.js';
import { HTTP_ADMIN_URL } from '@/config/app';
export default {
components: {
'jyf-parser': jyfParser,
},
dicts: ['sys_notice_type'],
data() {
return {
HTTP_ADMIN_URL,
loading: false,
noticeId: '',
notice: {},
@ -84,22 +100,19 @@ export default {
table: 'width:100%;border-collapse:collapse;',
video: 'max-width:100%;',
},
typeClassMap: {
1: 'tag-internal',
2: 'tag-external',
'__default__': ''
};
},
statusClassMap: {
0: 'status-draft',
1: 'status-submit',
'__default__': ''
computed: {
fileList() {
return Array.isArray(this.notice?.files) ? this.notice.files : [];
},
imageFiles() {
return this.fileList
.filter((item) => this.isImageFile(item))
.map((item) => this.getFileUrl(item))
.filter(Boolean);
},
smsClassMap: {
0: 'sms-unsent',
1: 'sms-sent',
'__default__': ''
}
};
},
onLoad(options) {
this.noticeId = options.noticeId || options.id || '';
@ -120,40 +133,83 @@ export default {
this.loading = false;
}
},
bizText(bizType, bizId) {
const t = bizType ? String(bizType) : '';
const id = bizId ? String(bizId) : '';
if (!t && !id) return '—';
if (t && id) return `${t} #${id}`;
return t || id;
getFilePath(item) {
if (!item) return '';
return item.attDir || item.sattDir || item.url || item.filePath || '';
},
formatDate(val) {
if (!val) return '';
if (typeof val === 'string') {
return val.replace('T', ' ').slice(0, 19);
}
if (val instanceof Date) {
const pad = (n) => (n < 10 ? '0' + n : '' + n);
return `${val.getFullYear()}-${pad(val.getMonth() + 1)}-${pad(val.getDate())} ${pad(
val.getHours()
)}:${pad(val.getMinutes())}`;
}
return String(val);
getFileUrl(item) {
const path = this.getFilePath(item);
if (!path) return '';
if (/^https?:\/\//i.test(path)) return path;
const base = String(this.HTTP_ADMIN_URL || '').replace(/\/$/, '');
return `${base}/${String(path).replace(/^\//, '')}`;
},
typeText(type) {
if (type === 1 || type === '1') return '内部';
if (type === 2 || type === '2') return '外部';
return type || '—';
isImageFile(item) {
const type = String(item?.attType || '').toLowerCase();
return (
['pic', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(type)
);
},
statusText(status) {
if (status === 0 || status === '0') return '草稿';
if (status === 1 || status === '1') return '提交';
return status ?? '—';
formatFileSize(size) {
const num = Number(size);
if (!num || Number.isNaN(num)) return '';
if (num < 1024) return `${num}B`;
if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)}KB`;
if (num < 1024 * 1024 * 1024) return `${(num / 1024 / 1024).toFixed(1)}MB`;
return `${(num / 1024 / 1024 / 1024).toFixed(1)}GB`;
},
smsText(smsStatus) {
if (smsStatus === 0 || smsStatus === '0') return '未发';
if (smsStatus === 1 || smsStatus === '1') return '已发';
return smsStatus ?? '—';
async fileClick(item) {
const current = this.getFileUrl(item);
if (!current) {
uni.showToast({
title: '附件地址无效',
icon: 'none',
});
return;
}
if (this.isImageFile(item)) {
uni.previewImage({
current,
urls: this.imageFiles,
fail: (e) => {
uni.showToast({
title: e?.errMsg || '预览失败',
icon: 'none',
});
},
});
return;
}
uni.showLoading({
title: '加载中...',
});
try {
const downloadRes = await new Promise((resolve, reject) => {
uni.downloadFile({
url: current,
success: (res) => resolve(res),
fail: reject,
});
});
if (downloadRes.statusCode !== 200) {
throw new Error('加载失败');
}
await new Promise((resolve, reject) => {
uni.openDocument({
filePath: downloadRes.tempFilePath,
showMenu: true,
success: resolve,
fail: reject,
});
});
} catch (e) {
uni.showToast({
title: e?.errMsg || e?.message || '打开失败',
icon: 'none',
});
} finally {
uni.hideLoading();
}
},
},
};
@ -184,49 +240,6 @@ export default {
word-break: break-all;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 14rpx;
}
}
.tag {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
line-height: 1;
}
.tag-internal {
background-color: #e6f7ff;
color: #409eff;
}
.tag-external {
background-color: #fff7e6;
color: #fa8c16;
}
.status-draft {
background-color: #f5f5f5;
color: #666;
}
.status-submit {
background-color: #f6ffed;
color: #52c41a;
}
.sms-unsent {
background-color: #f5f5f5;
color: #999;
}
.sms-sent {
background-color: #e6fffb;
color: #13c2c2;
}
.info-list {
@ -261,6 +274,91 @@ export default {
border-top: 1rpx solid #f5f5f5;
}
.attach-box {
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f5f5f5;
.attach-title {
font-size: 28rpx;
font-weight: 700;
color: #333;
margin-bottom: 16rpx;
}
.attach-list {
.attach-item {
min-height: 108rpx;
border: 1rpx solid #f0f0f0;
border-radius: 10rpx;
padding: 14rpx 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
background: #fafafa;
.left {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.thumb {
width: 72rpx;
height: 72rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
margin-right: 14rpx;
flex-shrink: 0;
}
.file-icon {
width: 72rpx;
height: 72rpx;
border-radius: 8rpx;
background: #eef4ff;
color: #409eff;
font-size: 30rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14rpx;
flex-shrink: 0;
}
.meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.name {
font-size: 24rpx;
color: #333;
line-height: 1.4;
word-break: break-all;
}
.desc {
margin-top: 6rpx;
font-size: 20rpx;
color: #999;
}
.preview-btn {
margin-left: 16rpx;
color: #409eff;
font-size: 24rpx;
flex-shrink: 0;
}
}
}
}
.empty-page {
margin-top: 120rpx;
display: flex;

Loading…
Cancel
Save