Compare commits
6 Commits
b443454bab
...
health-wei
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeb1e39920 | ||
|
|
6aaf564800 | ||
|
|
6846ea3e0f | ||
|
|
9027869e97 | ||
|
|
be3135b166 | ||
|
|
2297b19c2b |
@@ -38,7 +38,8 @@
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"clean:linux": "rm -rf dist || rm -rf node_modules",
|
||||
"clean:windows": "rd /s /q dist || rd /s /q node_modules"
|
||||
"clean:windows": "rd /s /q dist || rd /s /q node_modules",
|
||||
"postinstall": "node scripts/patch-u-input.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4000820240401001",
|
||||
|
||||
27
scripts/patch-u-input.js
Normal file
27
scripts/patch-u-input.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* uView u-input 组件补丁
|
||||
* 直接用预存的补丁文件覆盖原始文件
|
||||
* 使下拉选择框(readonly/disabled)超长文字可自动换行完整显示
|
||||
*
|
||||
* 修改内容详见 scripts/u-input.vue(已修改好的完整文件)
|
||||
*
|
||||
* 通过 package.json 的 postinstall 自动执行
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const target = path.join('node_modules', 'uview-plus', 'components', 'u-input', 'u-input.vue');
|
||||
const source = path.join('scripts', 'u-input.vue');
|
||||
|
||||
if (!fs.existsSync(target)) {
|
||||
console.log('[patch-u-input] target not found, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(source)) {
|
||||
console.log('[patch-u-input] source not found, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fs.copyFileSync(source, target);
|
||||
console.log('[patch-u-input] Patched successfully');
|
||||
447
scripts/u-input.vue
Normal file
447
scripts/u-input.vue
Normal file
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-input"
|
||||
:class="[inputClass, isSelectLike ? 'u-input--selectlike' : '']"
|
||||
:style="[wrapperStyle]"
|
||||
>
|
||||
<view class="u-input__content">
|
||||
<view
|
||||
class="u-input__content__prefix-icon"
|
||||
v-if="prefixIcon || $slots.prefix"
|
||||
>
|
||||
<slot name="prefix">
|
||||
<u-icon
|
||||
:name="prefixIcon"
|
||||
size="18"
|
||||
:customStyle="prefixIconStyle"
|
||||
></u-icon>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="u-input__content__field-wrapper" @tap="clickHandler">
|
||||
<!-- 根据uni-app的input组件文档,H5和APP中只要声明了password参数(无论true还是false),type均失效,此时
|
||||
为了防止type=number时,又存在password属性,type无效,此时需要设置password为undefined
|
||||
-->
|
||||
<!-- 选择型(readonly/disabled)用 view 显示文字,支持自动换行 -->
|
||||
<view
|
||||
v-if="isSelectLike"
|
||||
class="u-input__content__field-wrapper__field u-input__content__field-wrapper__field--selectlike"
|
||||
:style="[inputStyle]"
|
||||
>
|
||||
<text v-if="innerValue" class="u-input__content__field-wrapper__field__text">{{ innerValue }}</text>
|
||||
<text v-else class="u-input__content__field-wrapper__field__placeholder">{{ placeholder }}</text>
|
||||
</view>
|
||||
<!-- 普通输入模式保持原生 input -->
|
||||
<input
|
||||
v-else
|
||||
class="u-input__content__field-wrapper__field"
|
||||
:style="[inputStyle]"
|
||||
:type="type"
|
||||
:focus="focus"
|
||||
:cursor="cursor"
|
||||
:value="innerValue"
|
||||
:auto-blur="autoBlur"
|
||||
:disabled="disabled || readonly"
|
||||
:maxlength="maxlength"
|
||||
:placeholder="placeholder"
|
||||
:placeholder-style="placeholderStyle"
|
||||
:placeholder-class="placeholderClass"
|
||||
:confirm-type="confirmType"
|
||||
:confirm-hold="confirmHold"
|
||||
:hold-keyboard="holdKeyboard"
|
||||
:cursor-spacing="cursorSpacing"
|
||||
:adjust-position="adjustPosition"
|
||||
:selection-end="selectionEnd"
|
||||
:selection-start="selectionStart"
|
||||
:password="password || type === 'password' || undefined"
|
||||
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@confirm="onConfirm"
|
||||
@keyboardheightchange="onkeyboardheightchange"
|
||||
@change="onInput"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="u-input__content__clear"
|
||||
v-if="isShowClear"
|
||||
@click="onClear"
|
||||
>
|
||||
<u-icon
|
||||
name="close"
|
||||
size="11"
|
||||
color="#ffffff"
|
||||
customStyle="line-height: 12px"
|
||||
></u-icon>
|
||||
</view>
|
||||
<view
|
||||
class="u-input__content__subfix-icon"
|
||||
v-if="suffixIcon || $slots.suffix"
|
||||
>
|
||||
<slot name="suffix">
|
||||
<u-icon
|
||||
:name="suffixIcon"
|
||||
size="18"
|
||||
:customStyle="suffixIconStyle"
|
||||
></u-icon>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from "./props.js";
|
||||
import mpMixin from '../../libs/mixin/mpMixin.js';
|
||||
import mixin from '../../libs/mixin/mixin.js';
|
||||
/**
|
||||
* Input 输入框
|
||||
* @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
|
||||
* @tutorial https://uiadmin.net/uview-plus/components/input.html
|
||||
* @property {String | Number} value 输入的值
|
||||
* @property {String} type 输入框类型,见上方说明 ( 默认 'text' )
|
||||
* @property {Boolean} fixed 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true,兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序 ( 默认 false )
|
||||
* @property {Boolean} disabled 是否禁用输入框 ( 默认 false )
|
||||
* @property {String} disabledColor 禁用状态时的背景色( 默认 '#f5f7fa' )
|
||||
* @property {Boolean} clearable 是否显示清除控件 ( 默认 false )
|
||||
* @property {Boolean} password 是否密码类型 ( 默认 false )
|
||||
* @property {String | Number} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度 ( 默认 -1 )
|
||||
* @property {String} placeholder 输入框为空时的占位符
|
||||
* @property {String} placeholderClass 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/ ( 默认 'input-placeholder' )
|
||||
* @property {String | Object} placeholderStyle 指定placeholder的样式,字符串/对象形式,如"color: red;"
|
||||
* @property {Boolean} showWordLimit 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 ( 默认 false )
|
||||
* @property {String} confirmType 设置右下角按钮的文字,兼容性详见uni-app文档 ( 默认 'done' )
|
||||
* @property {Boolean} confirmHold 点击键盘右下角按钮时是否保持键盘不收起,H5无效 ( 默认 false )
|
||||
* @property {Boolean} holdKeyboard focus时,点击页面的时候不收起键盘,微信小程序有效 ( 默认 false )
|
||||
* @property {Boolean} focus 自动获取焦点,在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 ( 默认 false )
|
||||
* @property {Boolean} autoBlur 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效 ( 默认 false )
|
||||
* @property {Boolean} disableDefaultPadding 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效 ( 默认 false )
|
||||
* @property {String | Number} cursor 指定focus时光标的位置( 默认 -1 )
|
||||
* @property {String | Number} cursorSpacing 输入框聚焦时底部与键盘的距离 ( 默认 30 )
|
||||
* @property {String | Number} selectionStart 光标起始位置,自动聚集时有效,需与selection-end搭配使用 ( 默认 -1 )
|
||||
* @property {String | Number} selectionEnd 光标结束位置,自动聚集时有效,需与selection-start搭配使用 ( 默认 -1 )
|
||||
* @property {Boolean} adjustPosition 键盘弹起时,是否自动上推页面 ( 默认 true )
|
||||
* @property {String} inputAlign 输入框内容对齐方式( 默认 'left' )
|
||||
* @property {String | Number} fontSize 输入框字体的大小 ( 默认 '15px' )
|
||||
* @property {String} color 输入框字体颜色 ( 默认 '#303133' )
|
||||
* @property {Function} formatter 内容式化函数
|
||||
* @property {String} prefixIcon 输入框前置图标
|
||||
* @property {String | Object} prefixIconStyle 前置图标样式,对象或字符串
|
||||
* @property {String} suffixIcon 输入框后置图标
|
||||
* @property {String | Object} suffixIconStyle 后置图标样式,对象或字符串
|
||||
* @property {String} border 边框类型,surround-四周边框,bottom-底部边框,none-无边框 ( 默认 'surround' )
|
||||
* @property {Boolean} readonly 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会 ( 默认 false )
|
||||
* @property {String} shape 输入框形状,circle-圆形,square-方形 ( 默认 'square' )
|
||||
* @property {Object} customStyle 定义需要用到的外部样式
|
||||
* @property {Boolean} ignoreCompositionEvent 是否忽略组件内对文本合成系统事件的处理。
|
||||
* @example <u-input v-model="value" :password="true" suffix-icon="lock-fill" />
|
||||
*/
|
||||
export default {
|
||||
name: "u-input",
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
// 输入框的值
|
||||
innerValue: "",
|
||||
// 是否处于获得焦点状态
|
||||
focused: false,
|
||||
// value是否第一次变化,在watch中,由于加入immediate属性,会在第一次触发,此时不应该认为value发生了变化
|
||||
firstChange: true,
|
||||
// value绑定值的变化是由内部还是外部引起的
|
||||
changeFromInner: false,
|
||||
// 过滤处理方法
|
||||
innerFormatter: value => value
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// #ifdef VUE3
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
this.innerValue = newVal;
|
||||
/* #ifdef H5 */
|
||||
// 在H5中,外部value变化后,修改input中的值,不会触发@input事件,此时手动调用值变化方法
|
||||
if (
|
||||
this.firstChange === false &&
|
||||
this.changeFromInner === false
|
||||
) {
|
||||
this.valueChange();
|
||||
} else {
|
||||
// 尝试调用u-form的验证方法
|
||||
uni.$u.formValidate(this, "change");
|
||||
}
|
||||
/* #endif */
|
||||
this.firstChange = false;
|
||||
// 重置changeFromInner的值为false,标识下一次引起默认为外部引起的
|
||||
this.changeFromInner = false;
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
// #ifdef VUE2
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
this.innerValue = newVal;
|
||||
/* #ifdef H5 */
|
||||
// 在H5中,外部value变化后,修改input中的值,不会触发@input事件,此时手动调用值变化方法
|
||||
if (
|
||||
this.firstChange === false &&
|
||||
this.changeFromInner === false
|
||||
) {
|
||||
this.valueChange();
|
||||
} else {
|
||||
// 尝试调用u-form的验证方法
|
||||
uni.$u.formValidate(this, "change");
|
||||
}
|
||||
/* #endif */
|
||||
this.firstChange = false;
|
||||
// 重置changeFromInner的值为false,标识下一次引起默认为外部引起的
|
||||
this.changeFromInner = false;
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
isSelectLike() {
|
||||
return this.type === 'select' || this.disabled || this.readonly
|
||||
},
|
||||
// 是否显示清除控件
|
||||
isShowClear() {
|
||||
const { clearable, readonly, focused, innerValue } = this;
|
||||
return !!clearable && !readonly && !!focused && innerValue !== "";
|
||||
},
|
||||
// 组件的类名
|
||||
inputClass() {
|
||||
let classes = [],
|
||||
{ border, disabled, shape } = this;
|
||||
border === "surround" &&
|
||||
(classes = classes.concat(["u-border", "u-input--radius"]));
|
||||
classes.push(`u-input--${shape}`);
|
||||
border === "bottom" &&
|
||||
(classes = classes.concat([
|
||||
"u-border-bottom",
|
||||
"u-input--no-radius",
|
||||
]));
|
||||
return classes.join(" ");
|
||||
},
|
||||
// 组件的样式
|
||||
wrapperStyle() {
|
||||
const style = {};
|
||||
// 禁用状态下,被背景色加上对应的样式
|
||||
if (this.disabled) {
|
||||
style.backgroundColor = this.disabledColor;
|
||||
}
|
||||
// 无边框时,去除内边距
|
||||
if (this.border === "none") {
|
||||
style.padding = "0";
|
||||
} else {
|
||||
// 由于uni-app的iOS开发者能力有限,导致需要分开写才有效
|
||||
style.paddingTop = "6px";
|
||||
style.paddingBottom = "6px";
|
||||
style.paddingLeft = "9px";
|
||||
style.paddingRight = "9px";
|
||||
}
|
||||
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
|
||||
},
|
||||
// 输入框的样式
|
||||
inputStyle() {
|
||||
const style = {
|
||||
color: this.color,
|
||||
fontSize: uni.$u.addUnit(this.fontSize),
|
||||
textAlign: this.inputAlign
|
||||
};
|
||||
return style;
|
||||
},
|
||||
},
|
||||
// #ifdef VUE3
|
||||
emits: ['update:modelValue', 'focus', 'blur', 'change', 'confirm', 'clear', 'keyboardheightchange', 'click'],
|
||||
// #endif
|
||||
methods: {
|
||||
// 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
|
||||
setFormatter(e) {
|
||||
this.innerFormatter = e
|
||||
},
|
||||
// 当键盘输入时,触发input事件
|
||||
onInput(e) {
|
||||
let { value = "" } = e.detail || {};
|
||||
// 格式化过滤方法
|
||||
const formatter = this.formatter || this.innerFormatter
|
||||
const formatValue = formatter(value)
|
||||
// 为了避免props的单向数据流特性,需要先将innerValue值设置为当前值,再在$nextTick中重新赋予设置后的值才有效
|
||||
this.innerValue = value
|
||||
this.$nextTick(() => {
|
||||
this.innerValue = formatValue;
|
||||
this.valueChange();
|
||||
})
|
||||
},
|
||||
// 输入框失去焦点时触发
|
||||
onBlur(event) {
|
||||
this.$emit("blur", event.detail.value);
|
||||
// H5端的blur会先于点击清除控件的点击click事件触发,导致focused
|
||||
// 瞬间为false,从而隐藏了清除控件而无法被点击到
|
||||
uni.$u.sleep(150).then(() => {
|
||||
this.focused = false;
|
||||
});
|
||||
// 尝试调用u-form的验证方法
|
||||
uni.$u.formValidate(this, "blur");
|
||||
},
|
||||
// 输入框聚焦时触发
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.$emit("focus");
|
||||
},
|
||||
// 点击完成按钮时触发
|
||||
onConfirm(event) {
|
||||
this.$emit("confirm", this.innerValue);
|
||||
},
|
||||
// 键盘高度发生变化的时候触发此事件
|
||||
// 兼容性:微信小程序2.7.0+、App 3.1.0+
|
||||
onkeyboardheightchange(event) {
|
||||
this.$emit("keyboardheightchange", event);
|
||||
},
|
||||
// 内容发生变化,进行处理
|
||||
valueChange() {
|
||||
const value = this.innerValue;
|
||||
this.$nextTick(() => {
|
||||
// #ifdef VUE3
|
||||
this.$emit("update:modelValue", value);
|
||||
// #endif
|
||||
// #ifdef VUE2
|
||||
this.$emit("input", value);
|
||||
// #endif
|
||||
// 标识value值的变化是由内部引起的
|
||||
this.changeFromInner = true;
|
||||
this.$emit("change", value);
|
||||
// 尝试调用u-form的验证方法
|
||||
uni.$u.formValidate(this, "change");
|
||||
});
|
||||
},
|
||||
// 点击清除控件
|
||||
onClear() {
|
||||
this.innerValue = "";
|
||||
this.$nextTick(() => {
|
||||
this.valueChange();
|
||||
this.$emit("clear");
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 在安卓nvue上,事件无法冒泡
|
||||
* 在某些时间,我们希望监听u-from-item的点击事件,此时会导致点击u-form-item内的u-input后
|
||||
* 无法触发u-form-item的点击事件,这里通过手动调用u-form-item的方法进行触发
|
||||
*/
|
||||
clickHandler() {
|
||||
this.$emit('click')
|
||||
// #ifdef APP-NVUE
|
||||
if (uni.$u.os() === "android") {
|
||||
const formItem = uni.$u.$parent.call(this, "u-form-item");
|
||||
if (formItem) {
|
||||
formItem.clickHandler();
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
|
||||
.u-input {
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
|
||||
&--selectlike {
|
||||
/* 覆盖 inline style 的固定 height,让选择型模式高度自适应 */
|
||||
/* 短文字垂直居中,长文字撑开高度自动换行 */
|
||||
height: auto !important;
|
||||
min-height: 68rpx;
|
||||
|
||||
.u-input__content,
|
||||
.u-input__content__field-wrapper,
|
||||
.u-input__content__field-wrapper__field {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--radius,
|
||||
&--square {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&--no-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&--circle {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__field-wrapper {
|
||||
position: relative;
|
||||
@include flex(row);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
|
||||
&__field {
|
||||
line-height: 26px;
|
||||
text-align: left;
|
||||
color: $u-main-color;
|
||||
height: 24px;
|
||||
font-size: 15px;
|
||||
flex: 1;
|
||||
|
||||
/* 选择型模式:用 view 代替 input,支持自动换行 */
|
||||
&--selectlike {
|
||||
min-height: 24px;
|
||||
height: auto !important;
|
||||
line-height: 26px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $u-main-color;
|
||||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: $u-light-color;
|
||||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 100px;
|
||||
background-color: #c6c7cb;
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: scale(0.82);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__subfix-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__prefix-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
466
src/pages.json
466
src/pages.json
@@ -62,12 +62,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/statistic/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine",
|
||||
"style": {
|
||||
@@ -75,160 +69,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/doctorRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就医记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/doctorRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就医记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/doctorRecord/costList",
|
||||
"style": {
|
||||
"navigationBarTitleText": "费用明细"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/doctorRecord/costAddEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "费用明细"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/healthRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康档案"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/healthRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康档案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/healthRecord/details",
|
||||
"style": {
|
||||
"navigationBarTitleText": "档案统计"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/marRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用药记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/marRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用药记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/medicineBasic/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品基础信息"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/medicineBasic/details",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品基础信息详情"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/medicineBasic/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品基础信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/medicineStockIn/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品入库清单"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/medicineStockIn/details",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品入库清单详情"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/medicineStockIn/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品入库清单"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/health/medicineStock/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "药品实时库存"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/temperatureRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "体温记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/temperatureRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "体温记录"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/processRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "档案过程记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/processRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "过程记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/heightWeightRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "身高体重记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/milkPowderRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "吃奶记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/milkPowderRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "吃奶记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/heightWeightRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "身高体重记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/person/list",
|
||||
"style": {
|
||||
@@ -256,83 +96,44 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/statistic/doctorStatistic/index",
|
||||
"path": "pages/health/temperatureRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就医统计"
|
||||
"navigationBarTitleText": "体温记录"
|
||||
}
|
||||
} ,
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/statistic/healthStatistic/index",
|
||||
"path": "pages/health/temperatureRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "档案统计"
|
||||
"navigationBarTitleText": "体温记录"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/heightWeightRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "身高体重记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/milkPowderRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "吃奶记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/statistic/healthScreen/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康总览态势"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/statistic/recordScreen/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康档案态势"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/statistic/marStatistic/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用药统计"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/statistic/temperatureStatistic/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "体温统计"
|
||||
}
|
||||
} ,
|
||||
{
|
||||
"path": "pages/health/statistic/milkPowderStatistic/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "吃奶量统计"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/medicationPlan/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用药计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/medicationPlan/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用药计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/chronicDisease/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "慢性疾病档案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/chronicDisease/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "慢性疾病档案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/chronicDisease/details",
|
||||
"style": {
|
||||
"navigationBarTitleText": "慢性疾病详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/medicationTask/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "服药任务"
|
||||
}
|
||||
},
|
||||
"path": "pages/health/milkPowderRecord/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "吃奶记录"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/health/heightWeightRecord/addEdit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "身高体重记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/health/healthTags/list",
|
||||
"style": {
|
||||
@@ -393,9 +194,212 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/statistic",
|
||||
"name": "statistic",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index",
|
||||
"style": { "navigationBarTitleText": "统计分析" }
|
||||
},
|
||||
{
|
||||
"path": "doctorStatistic/index",
|
||||
"style": { "navigationBarTitleText": "就医统计" }
|
||||
},
|
||||
{
|
||||
"path": "healthStatistic/index",
|
||||
"style": { "navigationBarTitleText": "档案统计" }
|
||||
},
|
||||
{
|
||||
"path": "healthScreen/index",
|
||||
"style": { "navigationBarTitleText": "健康总览态势" }
|
||||
},
|
||||
{
|
||||
"path": "recordScreen/index",
|
||||
"style": { "navigationBarTitleText": "健康档案态势" }
|
||||
},
|
||||
{
|
||||
"path": "marStatistic/index",
|
||||
"style": { "navigationBarTitleText": "用药统计" }
|
||||
},
|
||||
{
|
||||
"path": "temperatureStatistic/index",
|
||||
"style": { "navigationBarTitleText": "体温统计" }
|
||||
},
|
||||
{
|
||||
"path": "milkPowderStatistic/index",
|
||||
"style": { "navigationBarTitleText": "吃奶量统计" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/medicineBasic",
|
||||
"name": "medicineBasic",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "药品基础信息" }
|
||||
},
|
||||
{
|
||||
"path": "details",
|
||||
"style": { "navigationBarTitleText": "药品基础信息详情" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "药品基础信息" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/medicineStockIn",
|
||||
"name": "medicineStockIn",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "药品入库清单" }
|
||||
},
|
||||
{
|
||||
"path": "details",
|
||||
"style": { "navigationBarTitleText": "药品入库清单详情" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "药品入库清单" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/medicineStock",
|
||||
"name": "medicineStock",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "药品实时库存" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/medicationPlan",
|
||||
"name": "medicationPlan",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "用药计划" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "用药计划" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/medicationTask",
|
||||
"name": "medicationTask",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "服药任务" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/marRecord",
|
||||
"name": "marRecord",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "用药记录" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "用药记录" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/doctorRecord",
|
||||
"name": "doctorRecord",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "就医记录" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "就医记录" }
|
||||
},
|
||||
{
|
||||
"path": "costList",
|
||||
"style": { "navigationBarTitleText": "费用明细" }
|
||||
},
|
||||
{
|
||||
"path": "costAddEdit",
|
||||
"style": { "navigationBarTitleText": "费用明细" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/healthRecord",
|
||||
"name": "healthRecord",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "健康档案" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "健康档案" }
|
||||
},
|
||||
{
|
||||
"path": "details",
|
||||
"style": { "navigationBarTitleText": "档案统计" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/processRecord",
|
||||
"name": "processRecord",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "档案过程记录" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "过程记录" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/health/chronicDisease",
|
||||
"name": "chronicDisease",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": { "navigationBarTitleText": "慢性疾病档案" }
|
||||
},
|
||||
{
|
||||
"path": "addEdit",
|
||||
"style": { "navigationBarTitleText": "慢性疾病档案" }
|
||||
},
|
||||
{
|
||||
"path": "details",
|
||||
"style": { "navigationBarTitleText": "慢性疾病详情" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
],
|
||||
"preloadRule": {
|
||||
"pages/health/index": {
|
||||
"network": "all",
|
||||
"packages": ["medicineBasic", "medicineStockIn", "medicineStock", "medicationPlan", "medicationTask", "marRecord", "doctorRecord", "healthRecord", "processRecord", "chronicDisease"]
|
||||
},
|
||||
"pages/health/statistic/index": {
|
||||
"network": "all",
|
||||
"packages": ["statistic"]
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#000000",
|
||||
"selectedColor": "#000000",
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
<script setup>
|
||||
import { getActivity, addActivity, updateActivity, uploadFile } from '@/api/health/activity'
|
||||
import config from '@/config'
|
||||
import { compressImage } from '@/utils/imageCompress'
|
||||
const { proxy } = getCurrentInstance()
|
||||
import { getDicts } from '@/api/system/dict/data.js'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -376,38 +377,56 @@ function chooseImage() {
|
||||
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
|
||||
return
|
||||
}
|
||||
// H5 端传 camera 会触发 getUserMedia,可能卡死;微信小程序/APP 需要相机选项
|
||||
let sourceType
|
||||
// #ifdef H5
|
||||
sourceType = ['album']
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
sourceType = ['album', 'camera']
|
||||
// #endif
|
||||
uni.chooseImage({
|
||||
count: 9 - attachmentList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
sourceType,
|
||||
success: (res) => {
|
||||
uploadImages(res.tempFilePaths, 0)
|
||||
},
|
||||
fail: (err) => {
|
||||
// 用户取消选择不走 success,也不会报错,但某些浏览器会触发 fail
|
||||
if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) {
|
||||
proxy.$refs['uToast'].show({ message: '选择图片失败', type: 'warning' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function uploadImages(paths, index) {
|
||||
async 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 => {
|
||||
uni.showLoading({ title: `处理中 ${index + 1}/${paths.length}`, mask: false })
|
||||
try {
|
||||
// 上传前先压缩(超过 500KB 才压,避免小图多走一道)
|
||||
const compressedPath = await compressImage(paths[index])
|
||||
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: false })
|
||||
const res = await uploadFile({
|
||||
filePath: compressedPath,
|
||||
name: 'file'
|
||||
})
|
||||
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(() => {
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uploading.value = false
|
||||
proxy.$refs['uToast'].show({ message: `第 ${index + 1} 张上传失败`, type: 'error' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function deleteImage(index) {
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<view class="operate" @click.stop>
|
||||
<view class="btn-summary" @click="handleCostSummary(item)">
|
||||
<uni-icons type="list" size="16" color="#13c2c2"></uni-icons>
|
||||
<text>费用汇总</text>
|
||||
<text>费用</text>
|
||||
</view>
|
||||
<view class="btn-edit" @click="handleEdit(item)">
|
||||
<uni-icons type="compose" size="16" color="#667eea"></uni-icons>
|
||||
@@ -454,21 +454,26 @@ function previewAttachment(urls, index) {
|
||||
// 活动特有 - 卡片头部布局
|
||||
.item-header {
|
||||
gap: 12rpx;
|
||||
/* 允许 header 内容换行,避免长地点/长名字被省略号截断 */
|
||||
align-items: flex-start;
|
||||
|
||||
.place-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.place-text {
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
/* 长地点名称最多2行后省略 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,9 +494,10 @@ function previewAttachment(urls, index) {
|
||||
|
||||
.name-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 40%;
|
||||
justify-content: flex-end;
|
||||
|
||||
.name-value {
|
||||
@@ -499,9 +505,13 @@ function previewAttachment(urls, index) {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
/* 长活动名最多2行后省略 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
}
|
||||
@@ -513,13 +523,6 @@ function previewAttachment(urls, index) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 活动特有 - 汇总按钮
|
||||
.operate .btn-summary {
|
||||
background: rgba(19, 194, 194, 0.1);
|
||||
color: #13c2c2;
|
||||
border: 1rpx solid rgba(19, 194, 194, 0.3);
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<script setup>
|
||||
import { getDoctorRecord, addDoctorRecord, updateDoctorRecord, uploadFile } from '@/api/health/doctorRecord'
|
||||
import config from '@/config'
|
||||
import { compressImage } from '@/utils/imageCompress'
|
||||
import { listPerson } from '@/api/health/person'
|
||||
import { listHealthRecord } from '@/api/health/healthRecord'
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -432,39 +433,57 @@ function chooseImage() {
|
||||
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
|
||||
return
|
||||
}
|
||||
// H5 端传 camera 会触发 getUserMedia,可能卡死;微信小程序/APP 需要相机选项
|
||||
let sourceType
|
||||
// #ifdef H5
|
||||
sourceType = ['album']
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
sourceType = ['album', 'camera']
|
||||
// #endif
|
||||
uni.chooseImage({
|
||||
count: 9 - attachmentList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
sourceType,
|
||||
success: (res) => {
|
||||
uploadImages(res.tempFilePaths, 0)
|
||||
},
|
||||
fail: (err) => {
|
||||
// 用户取消选择不走 success,也不会报错,但某些浏览器会触发 fail
|
||||
if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) {
|
||||
proxy.$refs['uToast'].show({ message: '选择图片失败', type: 'warning' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 递归上传图片
|
||||
function uploadImages(paths, index) {
|
||||
async 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 => {
|
||||
uni.showLoading({ title: `处理中 ${index + 1}/${paths.length}`, mask: false })
|
||||
try {
|
||||
// 上传前先压缩(超过 500KB 才压,避免小图多走一道)
|
||||
const compressedPath = await compressImage(paths[index])
|
||||
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: false })
|
||||
const res = await uploadFile({
|
||||
filePath: compressedPath,
|
||||
name: 'file'
|
||||
})
|
||||
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(() => {
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uploading.value = false
|
||||
proxy.$refs['uToast'].show({ message: `第 ${index + 1} 张上传失败`, type: 'error' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
|
||||
@@ -221,6 +221,35 @@ function handleDelete(item) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildCostDetailSummary() {
|
||||
if (!listData.value.length) return ''
|
||||
|
||||
// 按费用类型分组汇总
|
||||
const groupMap = new Map()
|
||||
listData.value.forEach(item => {
|
||||
const typeKey = item.type || '__other__'
|
||||
const typeLabel = item.type ? (dictStr(item.type, costTypeList.value) || '未分类') : '未分类'
|
||||
if (!groupMap.has(typeKey)) {
|
||||
groupMap.set(typeKey, { label: typeLabel, total: 0, count: 0 })
|
||||
}
|
||||
const g = groupMap.get(typeKey)
|
||||
g.total += Number(item.totalCost || 0)
|
||||
g.count += 1
|
||||
})
|
||||
|
||||
// 拼接汇总字符串,每行一个类型,末行合计
|
||||
const lines = []
|
||||
let grandTotal = 0
|
||||
let grandCount = 0
|
||||
groupMap.forEach(g => {
|
||||
lines.push(`${g.label}:${g.total.toFixed(2)}元(${g.count}笔)`)
|
||||
grandTotal += g.total
|
||||
grandCount += g.count
|
||||
})
|
||||
lines.push(`合计:${grandTotal.toFixed(2)}元(${grandCount}笔)`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function handleUpdateCost() {
|
||||
if (!doctorRecord.value.id) {
|
||||
uni.showToast({
|
||||
@@ -229,15 +258,38 @@ function handleUpdateCost() {
|
||||
})
|
||||
return
|
||||
}
|
||||
updateDoctorRecord({
|
||||
...doctorRecord.value,
|
||||
totalCost: Number(detailTotalCost.value)
|
||||
}).then(() => {
|
||||
doctorRecord.value.totalCost = Number(detailTotalCost.value)
|
||||
if (!listData.value.length) {
|
||||
uni.showToast({
|
||||
title: '汇总成功',
|
||||
icon: 'success'
|
||||
title: '暂无费用明细,无法汇总',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
const total = Number(detailTotalCost.value)
|
||||
const costDetail = buildCostDetailSummary()
|
||||
|
||||
// 会覆盖主表原有的费用明细字段,所以弹窗确认
|
||||
uni.showModal({
|
||||
title: '确认汇总',
|
||||
content: `总费用:${total.toFixed(2)}元\n费用明细将按类型汇总并覆盖原有内容,是否继续?`,
|
||||
confirmText: '确认汇总',
|
||||
cancelText: '取消',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
updateDoctorRecord({
|
||||
...doctorRecord.value,
|
||||
totalCost: total,
|
||||
costDetail: costDetail
|
||||
}).then(() => {
|
||||
doctorRecord.value.totalCost = total
|
||||
doctorRecord.value.costDetail = costDetail
|
||||
uni.showToast({
|
||||
title: '汇总成功',
|
||||
icon: 'success'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
<script setup>
|
||||
import {getMedicineBasic, addMedicineBasic, updateMedicineBasic, uploadFile } from '@/api/health/medicineBasic'
|
||||
import config from '@/config'
|
||||
import { compressImage } from '@/utils/imageCompress'
|
||||
const { proxy } = getCurrentInstance()
|
||||
import { getDicts } from '@/api/system/dict/data.js'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -592,28 +593,46 @@ function chooseImage() {
|
||||
proxy.$refs['uToast'].show({ message: '正在上传中,请稍候', type: 'warning' })
|
||||
return
|
||||
}
|
||||
// H5 端传 camera 会触发 getUserMedia,可能卡死;微信小程序/APP 需要相机选项
|
||||
let sourceType
|
||||
// #ifdef H5
|
||||
sourceType = ['album']
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
sourceType = ['album', 'camera']
|
||||
// #endif
|
||||
uni.chooseImage({
|
||||
count: 9 - attachmentList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
sourceType,
|
||||
success: (res) => {
|
||||
uploadImages(res.tempFilePaths, 0)
|
||||
},
|
||||
fail: (err) => {
|
||||
// 用户取消选择不走 success,也不会报错,但某些浏览器会触发 fail
|
||||
if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) {
|
||||
proxy.$refs['uToast'].show({ message: '选择图片失败', type: 'warning' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 递归上传图片
|
||||
function uploadImages(paths, index) {
|
||||
async 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 => {
|
||||
uni.showLoading({ title: `处理中 ${index + 1}/${paths.length}`, mask: false })
|
||||
try {
|
||||
// 上传前先压缩(超过 500KB 才压,避免小图多走一道)
|
||||
const compressedPath = await compressImage(paths[index])
|
||||
uni.showLoading({ title: `上传中 ${index + 1}/${paths.length}`, mask: false })
|
||||
const res = await uploadFile({
|
||||
filePath: compressedPath,
|
||||
name: 'file'
|
||||
})
|
||||
// 打印返回数据,便于调试
|
||||
console.log('==== upload response ====', JSON.stringify(res))
|
||||
// 兼容多种返回格式
|
||||
@@ -633,11 +652,11 @@ function uploadImages(paths, index) {
|
||||
}
|
||||
uni.hideLoading()
|
||||
uploadImages(paths, index + 1)
|
||||
}).catch(() => {
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uploading.value = false
|
||||
proxy.$refs['uToast'].show({ message: `第 ${index + 1} 张上传失败`, type: 'error' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
|
||||
@@ -217,8 +217,8 @@ const data = reactive({
|
||||
})
|
||||
const { filterPanel, queryParams} = toRefs(data)
|
||||
const windowHeight = computed(() => {
|
||||
uni.getSystemInfoSync().windowHeight - 50
|
||||
})
|
||||
return uni.getSystemInfoSync().windowHeight - 50
|
||||
})
|
||||
onLoad(() => {
|
||||
getList()
|
||||
});
|
||||
@@ -242,7 +242,7 @@ onLoad(() => {
|
||||
}
|
||||
|
||||
function selectType(item) {
|
||||
queryParams.value.type = item.dictValue
|
||||
queryParams.value.treatmentType = item.dictValue
|
||||
typeList.value.map(ele => {
|
||||
if (ele.dictValue == item.dictValue) {
|
||||
ele.selected = true
|
||||
@@ -313,9 +313,12 @@ function settingCancel() {
|
||||
settingPickShow.value = false
|
||||
}
|
||||
function searchSubmit() {
|
||||
if(queryParams.value.startTime!=''&&queryParams.value.startTime!=undefined&&queryParams.value.endTime!=''&&queryParams.value.endTime!=undefined){
|
||||
queryParams.value.time = queryParams.value.startTime+'-'+queryParams.value.endTime
|
||||
}
|
||||
// 只有开始和结束时间都设置时才拼接 time 字段,否则清空避免上一次筛选残留
|
||||
if (queryParams.value.startTime && queryParams.value.endTime) {
|
||||
queryParams.value.time = queryParams.value.startTime + '-' + queryParams.value.endTime
|
||||
} else {
|
||||
queryParams.value.time = ''
|
||||
}
|
||||
pageNum.value = 1
|
||||
listData.value = []
|
||||
getList()
|
||||
@@ -327,12 +330,15 @@ function settingCancel() {
|
||||
getList()
|
||||
}
|
||||
function resetQuery() {
|
||||
queryParams.value.type = ''
|
||||
queryParams.value.keys = ''
|
||||
queryParams.value.startTime = ''
|
||||
queryParams.value.time = ''
|
||||
|
||||
queryParams.value.endTime = ''
|
||||
queryParams.value.treatmentType = ''
|
||||
queryParams.value.keys = ''
|
||||
queryParams.value.startTime = ''
|
||||
queryParams.value.time = ''
|
||||
queryParams.value.endTime = ''
|
||||
// 同步重置选中状态
|
||||
typeList.value.forEach(ele => {
|
||||
Reflect.set(ele, 'selected', false)
|
||||
})
|
||||
}
|
||||
function enterDetails(item) {
|
||||
uni.navigateTo({ url: `/pages/health/medicineBasic/details?id=${item.id}` })
|
||||
|
||||
@@ -354,10 +354,12 @@ page {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
/* 超长药品名称自动换行,最多 3 行后省略 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
word-break: break-all;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
@@ -459,6 +461,8 @@ page {
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/* 关键:使用 flex-start 顶部对齐,避免同行两个 item 超长换行后高度不齐 */
|
||||
align-items: flex-start;
|
||||
margin: 0 -12rpx;
|
||||
|
||||
.info-item {
|
||||
@@ -491,12 +495,16 @@ page {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 两列布局下也允许换行(内容起长时撑开多行) */
|
||||
&:not(.info-item-full) .info-value {
|
||||
/* 超过 4 行后省略号截断,避免某个超长字段把整个卡片撑得太高 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,7 +520,8 @@ page {
|
||||
.btn-edit,
|
||||
.btn-delete,
|
||||
.btn-copy,
|
||||
.btn-detail {
|
||||
.btn-detail,
|
||||
.btn-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -552,6 +561,13 @@ page {
|
||||
color: $health-info;
|
||||
border: 1rpx solid $health-info-border;
|
||||
}
|
||||
|
||||
/* 汇总按钮:与 btn-detail 同一色调(信息色),避免各页面重复定义 */
|
||||
.btn-summary {
|
||||
background: $health-info-bg;
|
||||
color: $health-info;
|
||||
border: 1rpx solid $health-info-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
171
src/utils/imageCompress.ts
Normal file
171
src/utils/imageCompress.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 跨端图片压缩工具
|
||||
*
|
||||
* 用途:上传附件前压缩图片,避免大图(特别是手机原图 4MB+)导致上传卡死
|
||||
*
|
||||
* - H5:用 Canvas 重绘 + toDataURL('image/jpeg', quality)
|
||||
* - 小程序/APP:用 uni.compressImage API(quality + compressedWidth)
|
||||
*
|
||||
* 策略:
|
||||
* - 原图小于 500KB 不压(避免无谓耗时)
|
||||
* - 最大宽度限制 1280px(横屏照片)或按比例缩放
|
||||
* - JPEG 质量 0.75(体积约 1/5 ~ 1/10,肉眼几乎无差)
|
||||
* - 透明 PNG 也压成 JPEG(牺牲透明通道换取体积),如需透明可改 quality 调用
|
||||
*
|
||||
* 用法:
|
||||
* import { compressImage } from '@/utils/imageCompress'
|
||||
* const compressed = await compressImage(filePath)
|
||||
* uploadFile({ filePath: compressed, ... })
|
||||
*/
|
||||
|
||||
/** 不压缩的体积阈值(字节),800KB(小图直接跳过,避免无谓耗时) */
|
||||
const SKIP_COMPRESS_SIZE = 800 * 1024
|
||||
/** 最大边长,超过按比例缩放(保留宽高比)。1920px 兼顾清晰度与体积 */
|
||||
const MAX_SIDE = 1920
|
||||
/** JPEG 压缩质量 0~1。0.85 可保证医疗单据/药品说明书文字清晰 */
|
||||
const JPEG_QUALITY = 0.85
|
||||
|
||||
/**
|
||||
* H5 端压缩:Canvas 重绘 + toBlob
|
||||
*/
|
||||
function compressH5(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 计算缩放后的尺寸
|
||||
let { width, height } = img
|
||||
const longSide = Math.max(width, height)
|
||||
if (longSide > MAX_SIDE) {
|
||||
const scale = MAX_SIDE / longSide
|
||||
width = Math.round(width * scale)
|
||||
height = Math.round(height * scale)
|
||||
}
|
||||
// 创建 canvas 重绘
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
resolve(filePath) // 极端情况降级到原图
|
||||
return
|
||||
}
|
||||
// 白底(防止 PNG 透明通道转 JPEG 后变黑)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
// 转成 Blob 返回 ObjectURL(避免 base64 体积膨胀)
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
resolve(filePath)
|
||||
return
|
||||
}
|
||||
// 释放旧的 ObjectURL(如果是 blob: URL)
|
||||
if (filePath.startsWith('blob:')) {
|
||||
try { URL.revokeObjectURL(filePath) } catch (e) { /* ignore */ }
|
||||
}
|
||||
resolve(URL.createObjectURL(blob))
|
||||
},
|
||||
'image/jpeg',
|
||||
JPEG_QUALITY
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn('[imageCompress] H5 压缩失败,降级原图', err)
|
||||
resolve(filePath)
|
||||
}
|
||||
}
|
||||
img.onerror = (err) => {
|
||||
console.warn('[imageCompress] H5 图片加载失败,降级原图', err)
|
||||
resolve(filePath)
|
||||
}
|
||||
img.src = filePath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序/APP 端压缩:uni.compressImage
|
||||
*/
|
||||
function compressMP(filePath: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
uni.compressImage({
|
||||
src: filePath,
|
||||
quality: Math.round(JPEG_QUALITY * 100), // 75
|
||||
compressedWidth: MAX_SIDE,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
fail: (err) => {
|
||||
console.warn('[imageCompress] uni.compressImage 失败,降级原图', err)
|
||||
resolve(filePath) // 失败降级
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小(字节)。
|
||||
* - H5: blob: / data: URL 用 fetch 解析
|
||||
* - 小程序/APP: uni.getFileInfo
|
||||
*/
|
||||
function getFileSize(filePath: string): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef H5
|
||||
try {
|
||||
if (filePath.startsWith('blob:') || filePath.startsWith('data:')) {
|
||||
fetch(filePath)
|
||||
.then(r => r.blob())
|
||||
.then(b => resolve(b.size))
|
||||
.catch(() => resolve(0))
|
||||
return
|
||||
}
|
||||
} catch (e) { /* fallthrough */ }
|
||||
resolve(0) // 无法获取则不压缩(保守策略)
|
||||
return
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.getFileInfo({
|
||||
filePath,
|
||||
success: (res) => resolve(res.size),
|
||||
fail: () => resolve(0)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片入口(跨端)
|
||||
* @param filePath 图片临时路径
|
||||
* @returns 压缩后的图片路径(如失败则返回原路径)
|
||||
*/
|
||||
export async function compressImage(filePath: string): Promise<string> {
|
||||
if (!filePath) return filePath
|
||||
try {
|
||||
// 检查体积,小图直接跳过
|
||||
const size = await getFileSize(filePath)
|
||||
if (size > 0 && size < SKIP_COMPRESS_SIZE) {
|
||||
return filePath
|
||||
}
|
||||
// #ifdef H5
|
||||
return await compressH5(filePath)
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return await compressMP(filePath)
|
||||
// #endif
|
||||
} catch (err) {
|
||||
console.warn('[imageCompress] 压缩过程异常,降级原图', err)
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量压缩图片(串行执行,避免一次性占用过多内存)
|
||||
* @param filePaths 图片路径数组
|
||||
* @returns 压缩后的路径数组(与入参顺序一致)
|
||||
*/
|
||||
export async function compressImages(filePaths: string[]): Promise<string[]> {
|
||||
const result: string[] = []
|
||||
for (const p of filePaths) {
|
||||
result.push(await compressImage(p))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -31,7 +31,16 @@ const upload = <T>(config: RequestUploadConfig): Promise<ResponseData<T>> => {
|
||||
header: config.header,
|
||||
formData: config.formData,
|
||||
success: (res) => {
|
||||
let result = JSON.parse(res.data)
|
||||
let result
|
||||
// 后端可能返回 JSON 字符串(正常)或 HTML 错误页(如 413 Payload Too Large)
|
||||
// 必须包 try-catch,否则 JSON.parse 招错后 Promise 永远不 settle,mask loading 永不关闭
|
||||
try {
|
||||
result = JSON.parse(res.data)
|
||||
} catch (e) {
|
||||
toast('上传失败:服务器响应非 JSON 格式(可能文件过大)')
|
||||
reject('上传失败:响应解析失败')
|
||||
return
|
||||
}
|
||||
const code = result.code || 200
|
||||
// @ts-ignore
|
||||
const msg = errorCode[code] || result.msg || errorCode['default']
|
||||
|
||||
Reference in New Issue
Block a user