feat: 部分功能,新增附件上传功能,优化登录页面,修复bug。

This commit is contained in:
tianyongbao
2026-06-25 21:40:42 +08:00
parent 8f38ff8185
commit cdba9f6569
12 changed files with 833 additions and 18 deletions

View File

@@ -1,4 +1,14 @@
import request from '@/utils/request'
import upload from '@/utils/upload'
// 通用文件上传RuoYi-Cloud 文件接口)
export function uploadFile(data) {
return upload({
url: '/file/upload',
name: data.name || 'file',
filePath: data.filePath
})
}
// 查询活动记录列表
export function listActivity(query) {

View File

@@ -1,4 +1,14 @@
import request from '@/utils/request'
import upload from '@/utils/upload'
// 通用文件上传RuoYi-Cloud 文件接口)
export function uploadFile(data) {
return upload({
url: '/file/upload',
name: data.name || 'file',
filePath: data.filePath
})
}
// 查询就医记录列表
export function listDoctorRecord(query) {

View File

@@ -1,4 +1,14 @@
import request from '@/utils/request'
import upload from '@/utils/upload'
// 通用文件上传RuoYi-Cloud 文件接口)
export function uploadFile(data) {
return upload({
url: '/file/upload',
name: data.name || 'file',
filePath: data.filePath
})
}
// 查询药品基础信息列表
export function listMedicineBasic(query) {

View File

@@ -36,6 +36,18 @@
"navigationBarTitleText": "浏览文本"
}
},
{
"path": "pages/common/agreement/index",
"style": {
"navigationBarTitleText": "用户服务协议"
}
},
{
"path": "pages/common/privacy/index",
"style": {
"navigationBarTitleText": "隐私协议"
}
},
{
"path": "pages/health/homepage/index",
"style": {

View File

@@ -74,6 +74,23 @@
<u--textarea v-model="form.remark" height="80rpx" placeholder="请填写备注" inputAlign="left"
style="border: 2rpx solid #dcdfe6 !important; height: 160rpx;"></u--textarea>
</u-form-item>
<u-form-item label="附件图片" prop="attachment" labelPosition="top">
<view class="upload-wrap">
<view class="img-list">
<view class="img-item" v-for="(img, index) in attachmentList" :key="index">
<image :src="img" class="img-preview" mode="aspectFill" @click="previewImage(index)"></image>
<view class="img-del" @click="deleteImage(index)">
<uni-icons type="clear" size="20" color="#f5576c"></uni-icons>
</view>
</view>
<view class="img-add" v-if="attachmentList.length < 9" @click="chooseImage">
<uni-icons type="plusempty" size="28" color="#667eea"></uni-icons>
<text class="img-add-text">上传图片</text>
</view>
</view>
<view class="upload-tip">最多上传9张图片支持 jpg/png/jpeg 等格式</view>
</view>
</u-form-item>
</u--form>
<view class="form-btn">
<u-button type="primary" text="提交" @click="submit"></u-button>
@@ -105,8 +122,9 @@
</view>
</template>
<script setup>
import { getActivity, addActivity, updateActivity } from '@/api/health/activity'
<script setup>
import { getActivity, addActivity, updateActivity, uploadFile } from '@/api/health/activity'
import config from '@/config'
const { proxy } = getCurrentInstance()
import { getDicts } from '@/api/system/dict/data.js'
import dayjs from 'dayjs'
@@ -144,6 +162,15 @@ const getInputStyle = (field) => {
return errorFields.value.includes(field) ? inputErrorStyle : inputBaseStyle
}
function normalizeAttachmentUrl(url) {
if (!url) return ''
if (url.startsWith('http')) return url
// 附件是后端静态资源直链(如 /fileUrl/...、/profile/upload/...
// 需要用域名拼接,不能带网关前缀 prod-api否则会 404
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return origin + url
}
const birthdayShow = ref(false)
const title = ref("活动记录")
const startTimeShow = ref(false)
@@ -154,6 +181,8 @@ const showType = ref(false)
const flag = ref('add')
const activityVolumeList = ref([])
const showActivityVolume = ref(false)
const attachmentList = ref([])
const uploading = ref(false)
const data = reactive({
form: {
@@ -175,7 +204,8 @@ form: {
remark: null,
totalCost: 0,
partner: null,
costDetail: null
costDetail: null,
attachment: null
},
rules: {
name: [{ required: true, message: '活动名称不能为空', trigger:['change', 'blur'] }],
@@ -245,6 +275,9 @@ function getData() {
if(form.value.id!=null){
getActivity(form.value.id).then(res => {
form.value = res.data
if (form.value.attachment) {
attachmentList.value = form.value.attachment.split(',').filter(url => url).map(normalizeAttachmentUrl)
}
// 类型
getDicts('activity_type').then(result => {
form.value.typeName=dictStr(form.value.type, result.data)
@@ -298,6 +331,7 @@ function getData() {
function submit() {
// 清空错误字段
errorFields.value = []
form.value.attachment = attachmentList.value.join(',')
proxy.$refs['uForm'].validate().then(() => {
if (form.value.id != null) {
@@ -336,6 +370,65 @@ function submit() {
proxy.$modal.msgError('请填写完整信息')
})
}
function chooseImage() {
if (uploading.value) {
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
return
}
uni.chooseImage({
count: 9 - attachmentList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
uploadImages(res.tempFilePaths, 0)
}
})
}
function uploadImages(paths, index) {
if (index >= paths.length) {
uploading.value = false
return
}
uploading.value = true
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: true })
uploadFile({
filePath: paths[index],
name: 'file'
}).then(res => {
const url = (res.data && res.data.url) || res.fileName || res.url
if (url) {
attachmentList.value.push(normalizeAttachmentUrl(url))
}
uni.hideLoading()
uploadImages(paths, index + 1)
}).catch(() => {
uni.hideLoading()
uploading.value = false
proxy.$refs['uToast'].show({ message: `${index + 1} 张上传失败`, type: 'error' })
})
}
function deleteImage(index) {
uni.showModal({
title: '提示',
content: '确定删除这张图片吗?',
confirmColor: '#f5576c',
success: (modalRes) => {
if (modalRes.confirm) {
attachmentList.value.splice(index, 1)
}
}
})
}
function previewImage(index) {
uni.previewImage({
urls: attachmentList.value,
current: attachmentList.value[index]
})
}
</script>
<style lang="scss" scoped>
@@ -390,6 +483,69 @@ function submit() {
z-index: 10;
}
}
.upload-wrap {
width: 100%;
.img-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 12rpx;
}
.img-item {
position: relative;
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.img-preview {
width: 100%;
height: 100%;
display: block;
}
.img-del {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
display: flex;
align-items: center;
justify-content: center;
}
.img-add {
width: 180rpx;
height: 180rpx;
border: 2rpx dashed rgba(102, 126, 234, 0.55);
border-radius: 12rpx;
background: rgba(102, 126, 234, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
}
.img-add-text {
color: #667eea;
font-size: 24rpx;
}
.upload-tip {
font-size: 22rpx;
color: #909399;
line-height: 1.4;
}
}
}
}
</style>

View File

@@ -84,6 +84,19 @@
<text class="info-label">备注</text>
<text class="info-value">{{ item.remark }}</text>
</view>
<view class="info-item info-item-full" v-if="parseAttachment(item.attachment).length">
<text class="info-label">附件</text>
<view class="attachment-list">
<image
v-for="(img, imgIdx) in parseAttachment(item.attachment)"
:key="imgIdx"
:src="img"
class="attachment-thumb"
mode="aspectFill"
@click.stop="previewAttachment(parseAttachment(item.attachment), imgIdx)"
></image>
</view>
</view>
</view>
</view>
@@ -180,6 +193,7 @@ import { getDicts } from '@/api/system/dict/data.js'
import { timeHandler } from '@/utils/common.ts'
import {onLoad,onShow} from "@dcloudio/uni-app";
import dayjs from 'dayjs'
import config from '@/config'
// 计算属性与监听属性是在vue中而非uniap中 需要注意!!!
import {reactive ,toRefs,ref,computed }from "vue";
const pageNum = ref(1)
@@ -409,6 +423,23 @@ function getList() {
}
});
}
function parseAttachment(attachment) {
if (!attachment) return []
// 附件是后端静态资源直链,需要用域名拼接,不能带网关前缀 prod-api
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return attachment.split(',').filter(url => url).map(url => {
if (url.startsWith('http')) return url
return origin + url
})
}
function previewAttachment(urls, index) {
uni.previewImage({
urls: urls,
current: urls[index]
})
}
</script>
@@ -489,6 +520,27 @@ function getList() {
border: 1rpx solid rgba(19, 194, 194, 0.3);
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 4rpx;
width: 100%;
.attachment-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
// ==================== 费用汇总面板 ====================
.summary-mask {
position: fixed;

View File

@@ -70,6 +70,23 @@
<u-form-item label="备注" prop="remark" labelPosition="top">
<u--textarea v-model="form.remark" height="40rpx" placeholder="请填写备注" inputAlign="left" style="border: 2rpx solid #dcdfe6 !important; height: 160rpx;" :customStyle="getTextareaStyle('remark')"></u--textarea>
</u-form-item>
<u-form-item label="附件图片" prop="attachment" labelPosition="top">
<view class="upload-wrap">
<view class="img-list">
<view class="img-item" v-for="(img, index) in attachmentList" :key="index">
<image :src="img" class="img-preview" mode="aspectFill" @click="previewImage(index)"></image>
<view class="img-del" @click="deleteImage(index)">
<uni-icons type="clear" size="20" color="#f5576c"></uni-icons>
</view>
</view>
<view class="img-add" v-if="attachmentList.length < 9" @click="chooseImage">
<uni-icons type="plusempty" size="28" color="#667eea"></uni-icons>
<text class="img-add-text">上传图片</text>
</view>
</view>
<view class="upload-tip">最多上传9张图片支持 jpg/png/jpeg 等格式</view>
</view>
</u-form-item>
</u--form>
<view class="form-btn">
<u-button type="primary" text="提交" @click="submit"></u-button>
@@ -95,7 +112,8 @@
</template>
<script setup>
import { getDoctorRecord, addDoctorRecord, updateDoctorRecord } from '@/api/health/doctorRecord'
import { getDoctorRecord, addDoctorRecord, updateDoctorRecord, uploadFile } from '@/api/health/doctorRecord'
import config from '@/config'
import { listPerson } from '@/api/health/person'
import { listHealthRecord } from '@/api/health/healthRecord'
const { proxy } = getCurrentInstance()
@@ -113,6 +131,9 @@ const personList = ref([])
const typeList = ref([])
const showType = ref(false)
const flag = ref('add')
// 附件图片列表存储完整URL用于预览和提交
const attachmentList = ref([])
const uploading = ref(false)
const data = reactive({
form: {
id: null,
@@ -131,7 +152,8 @@ form: {
personId: null,
totalCost: 0,
partner: null,
costDetail: null
costDetail: null,
attachment: null
},
queryPersonParams: {
pageNum: 1,
@@ -205,6 +227,15 @@ const getTextareaStyle = (field) => {
return errorFields.value.includes(field) ? textareaErrorStyle : textareaBaseStyle
}
function normalizeAttachmentUrl(url) {
if (!url) return ''
if (url.startsWith('http')) return url
// 附件是后端静态资源直链(如 /fileUrl/...、/profile/upload/...
// 需要用域名拼接,不能带网关前缀 prod-api否则会 404
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return origin + url
}
onLoad((option) => {
form.value.id = option.id
@@ -246,6 +277,10 @@ function getData() {
})
})
form.value = res.data
// 加载已有附件到列表
if (form.value.attachment) {
attachmentList.value = form.value.attachment.split(',').filter(url => url).map(normalizeAttachmentUrl)
}
// 就医类型
getDicts('doctor_type').then(result => {
typeList.value =[result.data]
@@ -351,6 +386,8 @@ function datePickConfirm(e) {
datePickShow.value = false
}
function submit() {
// 提交前同步附件字段逗号分隔的URL字符串
form.value.attachment = attachmentList.value.join(',')
proxy.$refs['uForm'].validate().then(() => {
if (form.value.id != null) {
if(flag.value==null){
@@ -388,6 +425,69 @@ function submit() {
proxy.$modal.msgError('请填写完整信息')
})
}
// 选择图片并上传
function chooseImage() {
if (uploading.value) {
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
return
}
uni.chooseImage({
count: 9 - attachmentList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
uploadImages(res.tempFilePaths, 0)
}
})
}
// 递归上传图片
function uploadImages(paths, index) {
if (index >= paths.length) {
uploading.value = false
return
}
uploading.value = true
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: true })
uploadFile({
filePath: paths[index],
name: 'file'
}).then(res => {
const url = (res.data && res.data.url) || res.fileName || res.url
if (url) {
attachmentList.value.push(normalizeAttachmentUrl(url))
}
uni.hideLoading()
uploadImages(paths, index + 1)
}).catch(() => {
uni.hideLoading()
uploading.value = false
proxy.$refs['uToast'].show({ message: `${index + 1} 张上传失败`, type: 'error' })
})
}
// 删除图片
function deleteImage(index) {
uni.showModal({
title: '提示',
content: '确定删除这张图片吗?',
confirmColor: '#f5576c',
success: (modalRes) => {
if (modalRes.confirm) {
attachmentList.value.splice(index, 1)
}
}
})
}
// 预览图片
function previewImage(index) {
uni.previewImage({
urls: attachmentList.value,
current: attachmentList.value[index]
})
}
</script>
<style lang="scss" scoped>
@@ -446,6 +546,82 @@ page {
z-index: 10;
}
}
// 附件上传区域
.upload-wrap {
width: 100%;
.img-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 12rpx;
}
.img-item {
position: relative;
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.15);
.img-preview {
width: 100%;
height: 100%;
background: #f5f7fa;
}
.img-del {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 36rpx;
height: 36rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.15);
&:active {
transform: scale(0.9);
}
}
}
.img-add {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
border: 2rpx dashed rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
background: rgba(102, 126, 234, 0.12);
}
.img-add-text {
font-size: 22rpx;
color: #667eea;
font-weight: 500;
}
}
.upload-tip {
font-size: 22rpx;
color: #909399;
line-height: 1.4;
}
}
}
}
</style>
@@ -479,4 +655,4 @@ page {
border: 2rpx solid #f56c6c !important;
background: #fef0f0 !important;
}
</style>
</style>

View File

@@ -287,6 +287,10 @@ function getData() {
form.value.typeName = dictStr(form.value.type, costTypeList.value)
form.value.unitName = dictStr(form.value.unit, packageUnitList.value)
form.value.checkTypeName = dictStr(form.value.checkType, checkTypeList.value) || dictStr(form.value.checkType, nursingTypeList.value)
// 后端返回的单价/数量/总价为数字类型,校验器按 string 校验会误判为空,这里统一转为字符串
form.value.price = form.value.price != null ? String(form.value.price) : form.value.price
form.value.count = form.value.count != null ? String(form.value.count) : form.value.count
form.value.totalCost = form.value.totalCost != null ? String(form.value.totalCost) : form.value.totalCost
applyTypeState(form.value.type)
if (flag.value != null) {
form.value.id = null

View File

@@ -168,6 +168,19 @@
<text class="info-label">备注</text>
<text class="info-value">{{ item.remark }}</text>
</view>
<view class="info-item info-item-full" v-if="parseAttachment(item.attachment).length">
<text class="info-label">附件</text>
<view class="attachment-list">
<image
v-for="(img, imgIdx) in parseAttachment(item.attachment)"
:key="imgIdx"
:src="img"
class="attachment-thumb"
mode="aspectFill"
@click.stop="previewAttachment(parseAttachment(item.attachment), imgIdx)"
></image>
</view>
</view>
</view>
</view>
<view class="operate" @click.stop>
@@ -213,6 +226,7 @@ import { getDicts } from '@/api/system/dict/data.js'
import { timeHandler } from '@/utils/common.ts'
import {onLoad,onShow} from "@dcloudio/uni-app";
import dayjs from 'dayjs'
import config from '@/config'
// 计算属性与监听属性是在vue中而非uniap中 需要注意!!!
import {reactive ,toRefs,ref,computed }from "vue";
const pageNum = ref(1)
@@ -467,6 +481,25 @@ function settingCancel() {
}
});
}
// 解析附件字段为URL数组
function parseAttachment(attachment) {
if (!attachment) return []
// 附件是后端静态资源直链,需要用域名拼接,不能带网关前缀 prod-api
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return attachment.split(',').filter(url => url).map(url => {
if (url.startsWith('http')) return url
return origin + url
})
}
// 预览附件图片
function previewAttachment(urls, index) {
uni.previewImage({
urls: urls,
current: urls[index]
})
}
</script>
<style lang="scss" scoped>
@@ -476,4 +509,26 @@ function settingCancel() {
.filter-panel {
top: 180rpx;
}
// 附件图片展示区
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 4rpx;
width: 100%;
.attachment-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
</style>

View File

@@ -129,6 +129,23 @@
<u--textarea v-model="form.remark" placeholder="请填写备注" inputAlign="left"
style="border: 2rpx solid #dcdfe6 !important; height: 160rpx;"></u--textarea>
</u-form-item>
<u-form-item label="附件图片" prop="attachment" labelPosition="top">
<view class="upload-wrap">
<view class="img-list">
<view class="img-item" v-for="(img, index) in attachmentList" :key="index">
<image :src="img" class="img-preview" mode="aspectFill" @click="previewImage(index)"></image>
<view class="img-del" @click="deleteImage(index)">
<uni-icons type="clear" size="20" color="#f5576c"></uni-icons>
</view>
</view>
<view class="img-add" v-if="attachmentList.length < 9" @click="chooseImage">
<uni-icons type="plusempty" size="28" color="#667eea"></uni-icons>
<text class="img-add-text">上传图片</text>
</view>
</view>
<view class="upload-tip">最多上传9张图片支持 jpg/png/jpeg 等格式</view>
</view>
</u-form-item>
</u--form>
<view class="form-btn">
<u-button type="primary" text="提交" @click="submit"></u-button>
@@ -158,7 +175,8 @@
</template>
<script setup>
import {getMedicineBasic, addMedicineBasic, updateMedicineBasic } from '@/api/health/medicineBasic'
import {getMedicineBasic, addMedicineBasic, updateMedicineBasic, uploadFile } from '@/api/health/medicineBasic'
import config from '@/config'
const { proxy } = getCurrentInstance()
import { getDicts } from '@/api/system/dict/data.js'
import dayjs from 'dayjs'
@@ -196,10 +214,23 @@ const getInputStyle = (field) => {
return errorFields.value.includes(field) ? inputErrorStyle : inputBaseStyle
}
function normalizeAttachmentUrl(url) {
if (!url) return ''
if (url.startsWith('http')) return url
// 附件是后端静态资源直链(如 /fileUrl/...、/profile/upload/...
// 需要用域名拼接,不能带网关前缀 prod-api否则会 404
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return origin + url
}
const datePickShow = ref(false)
const title = ref("药品基础信息")
const flag = ref('add')
// 附件图片列表存储完整URL
const attachmentList = ref([])
const uploading = ref(false)
const dosageFormList = ref([])
const showDosageForm = ref(false)
@@ -254,7 +285,8 @@ form: {
contentUnit: null,
character: null,
storage: null,
indications: null
indications: null,
attachment: null
},
rules: {
name: [{ required: true, message: '药品全称不能为空', trigger:['change', 'blur'] }],
@@ -334,6 +366,10 @@ function getData() {
if(form.value.id!=null){
getMedicineBasic(form.value.id).then(res => {
form.value = res.data
// 加载已有附件到列表
if (form.value.attachment) {
attachmentList.value = form.value.attachment.split(',').filter(url => url).map(normalizeAttachmentUrl)
}
getDicts('dosage_form').then(result => {
form.value.dosageFormName=dictStr(form.value.dosageForm, result.data)
})
@@ -508,6 +544,8 @@ function handleDosageForm() {
function submit() {
// 清空错误字段
errorFields.value = []
// 提交前同步附件字段逗号分隔的URL字符串
form.value.attachment = attachmentList.value.join(',')
proxy.$refs['uForm'].validate().then(() => {
if (form.value.id != null) {
@@ -547,6 +585,82 @@ function submit() {
proxy.$modal.msgError('请填写完整信息')
})
}
// 选择图片并上传
function chooseImage() {
if (uploading.value) {
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
return
}
uni.chooseImage({
count: 9 - attachmentList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
uploadImages(res.tempFilePaths, 0)
}
})
}
// 递归上传图片
function uploadImages(paths, index) {
if (index >= paths.length) {
uploading.value = false
return
}
uploading.value = true
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: true })
uploadFile({
filePath: paths[index],
name: 'file'
}).then(res => {
// 打印返回数据,便于调试
console.log('==== upload response ====', JSON.stringify(res))
// 兼容多种返回格式
const url = (res.data && (res.data.url || res.data.fileName || res.data.imgUrl))
|| res.url
|| res.fileName
|| res.imgUrl
|| res.fileUrl
if (url) {
const fullUrl = normalizeAttachmentUrl(url)
console.log('==== push attachment url ====', fullUrl)
// 使用重新赋值确保响应式更新
attachmentList.value = [...attachmentList.value, fullUrl]
} else {
console.error('==== 上传成功但未找到URL字段请查看上方返回数据 ====', res)
proxy.$refs['uToast'].show({ message: '上传成功但未解析到图片地址', type: 'warning' })
}
uni.hideLoading()
uploadImages(paths, index + 1)
}).catch(() => {
uni.hideLoading()
uploading.value = false
proxy.$refs['uToast'].show({ message: `${index + 1} 张上传失败`, type: 'error' })
})
}
// 删除图片
function deleteImage(index) {
uni.showModal({
title: '提示',
content: '确定删除这张图片吗?',
confirmColor: '#f5576c',
success: (modalRes) => {
if (modalRes.confirm) {
attachmentList.value.splice(index, 1)
}
}
})
}
// 预览图片
function previewImage(index) {
uni.previewImage({
urls: attachmentList.value,
current: attachmentList.value[index]
})
}
</script>
<style lang="scss" scoped>
@@ -601,6 +715,82 @@ function submit() {
z-index: 10;
}
}
// 附件上传区域
.upload-wrap {
width: 100%;
.img-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 12rpx;
}
.img-item {
position: relative;
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.15);
.img-preview {
width: 100%;
height: 100%;
background: #f5f7fa;
}
.img-del {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 36rpx;
height: 36rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.15);
&:active {
transform: scale(0.9);
}
}
}
.img-add {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
border: 2rpx dashed rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
background: rgba(102, 126, 234, 0.12);
}
.img-add-text {
font-size: 22rpx;
color: #667eea;
font-weight: 500;
}
}
.upload-tip {
font-size: 22rpx;
color: #909399;
line-height: 1.4;
}
}
}
}
</style>

View File

@@ -130,6 +130,19 @@
<text class="info-label">录入时间</text>
<text class="info-value">{{ item.createTime || '--' }}</text>
</view>
<view class="info-item info-item-full" v-if="parseAttachment(item.attachment).length">
<text class="info-label">附件</text>
<view class="attachment-list">
<image
v-for="(img, imgIdx) in parseAttachment(item.attachment)"
:key="imgIdx"
:src="img"
class="attachment-thumb"
mode="aspectFill"
@click.stop="previewAttachment(parseAttachment(item.attachment), imgIdx)"
></image>
</view>
</view>
</view>
</view>
@@ -166,6 +179,7 @@ import { getDicts } from '@/api/system/dict/data.js'
import { timeHandler } from '@/utils/common.ts'
import {onLoad,onShow} from "@dcloudio/uni-app";
import auth from "@/plugins/auth"; // 建议使用auth进行鉴权操作
import config from '@/config'
import dayjs from 'dayjs'
// 计算属性与监听属性是在vue中而非uniap中 需要注意!!!
import {reactive ,toRefs,ref,computed }from "vue";
@@ -358,7 +372,25 @@ function settingCancel() {
}
});
}
// 解析附件字段为URL数组
function parseAttachment(attachment) {
if (!attachment) return []
// 附件是后端静态资源直链,需要用域名拼接,不能带网关前缀 prod-api
const origin = config.baseUrl.replace(/^(https?:\/\/[^/]+).*$/, '$1')
return attachment.split(',').filter(url => url).map(url => {
if (url.startsWith('http')) return url
return origin + url
})
}
// 预览附件图片
function previewAttachment(urls, index) {
uni.previewImage({
urls: urls,
current: urls[index]
})
}
</script>
<style lang="scss" scoped>
@@ -368,4 +400,26 @@ function settingCancel() {
.search-view .filter-panel {
top: 96rpx;
}
// 附件图片展示区
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 4rpx;
width: 100%;
.attachment-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
</style>

View File

@@ -38,16 +38,26 @@
/>
</view>
<!-- 用户协议 -->
<!-- 用户协议H5 端记住密码在同一行前面 -->
<view class="agreement-box">
<view class="agreement-row" @click="agreeChange">
<view :class="['checkbox-small', agree ? 'checked' : '']">
<uni-icons v-if="agree" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
<view class="agreement-line">
<!-- H5 端记住密码放前面 -->
<view v-if="isH5" class="agreement-row remember-part" @click="rememberChange">
<view :class="['checkbox-small', rememberPwd ? 'checked' : '']">
<uni-icons v-if="rememberPwd" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
</view>
<text class="agreement-text">记住密码</text>
</view>
<!-- 同意协议 -->
<view class="agreement-row agree-part" @click="agreeChange">
<view :class="['checkbox-small', agree ? 'checked' : '']">
<uni-icons v-if="agree" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
</view>
<text class="agreement-text">已阅读并同意</text>
<text class="link-text" @click.stop="handleUserAgrement">用户协议</text>
<text class="agreement-text"></text>
<text class="link-text" @click.stop="handlePrivacy">隐私协议</text>
</view>
<text class="agreement-text">已阅读并同意</text>
<text class="link-text" @click.stop="handleUserAgrement">用户协议</text>
<text class="agreement-text"></text>
<text class="link-text" @click.stop="handlePrivacy">隐私协议</text>
</view>
</view>
@@ -77,12 +87,23 @@ import { getWxCode } from '@/utils/geek';
import { wxLogin } from '@/api/oauth';
import { setToken } from '@/utils/auth';
import { encrypt, decrypt } from '@/utils/jsencrypt'
// ========== 平台判断(条件编译)==========
// H5 端为 true其他端为 false。条件编译保证仅 H5 打包对应逻辑
let isH5 = false
// #ifdef H5
isH5 = true
// #endif
const REMEMBER_KEY = 'remember_login_info'
const userStore = useUserStore()
const codeUrl = ref("");
const captchaEnabled = ref(true); // 是否开启验证码
const useWxLogin = ref(false); // 是否使用微信登录
const globalConfig = ref(config);
const agree = ref(false); // 同意协议
const agree = ref(isH5 ? true : false); // H5 端默认勾选同意协议
const rememberPwd = ref(false); // 记住密码
const loginForm = ref({
username: "",
password: "",
@@ -103,8 +124,49 @@ if (useWxLogin.value) {
}
// 页面加载时检查是否记住了密码
onMounted(() => {
loadRememberedLogin()
});
// 读取本地记住的登录信息(仅 H5
function loadRememberedLogin() {
if (!isH5) return
try {
const raw = uni.getStorageSync(REMEMBER_KEY)
if (!raw) return
const info = JSON.parse(raw)
if (info && info.username) {
loginForm.value.username = info.username
// 密码以 base64 存储,解码后回填
if (info.password) {
try {
loginForm.value.password = decodeURIComponent(escape(atob(info.password)))
} catch (e) {
loginForm.value.password = ''
}
}
rememberPwd.value = true
}
} catch (e) {
console.error('读取记住的登录信息失败', e)
uni.removeStorageSync(REMEMBER_KEY)
}
}
// 保存或清除记住的登录信息(仅 H5
function saveRememberedLogin() {
if (!isH5) return
if (rememberPwd.value && loginForm.value.username) {
const info = {
username: loginForm.value.username,
// 密码使用 base64 简单编码,避免明文存储
password: btoa(unescape(encodeURIComponent(loginForm.value.password || '')))
}
uni.setStorageSync(REMEMBER_KEY, JSON.stringify(info))
} else {
uni.removeStorageSync(REMEMBER_KEY)
}
}
// 获取图形验证码
function getCode() {
getCodeImg().then(res => {
@@ -137,6 +199,8 @@ async function handleLogin() {
async function pwdLogin() {
userStore.login(loginForm.value).then(() => {
modal.closeLoading()
// 登录成功后保存或清除记住的密码
saveRememberedLogin()
loginSuccess()
}).catch(() => {
if (captchaEnabled.value) {
@@ -158,6 +222,11 @@ function agreeChange(){
agree.value = !agree.value;
}
// 切换记住密码状态
function rememberChange(){
rememberPwd.value = !rememberPwd.value;
}
// 隐私协议
function handlePrivacy() {
uni.navigateTo({
@@ -266,12 +335,29 @@ page {
margin-top: 24rpx;
padding: 0 4rpx;
.agreement-line {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
line-height: 1.6;
}
.agreement-row {
display: flex;
align-items: center;
flex-wrap: wrap;
line-height: 1.6;
&.remember-part {
margin-right: 32rpx;
flex-shrink: 0;
}
&.agree-part {
flex: 1;
min-width: 0;
}
.checkbox-small {
width: 28rpx;
height: 28rpx;