master
lion 1 month ago
parent d65ba691f6
commit 5d3631f79e

@ -1,13 +1,14 @@
const mode = process.env.NODE_ENV;
// const mode = 'development';
let ROOTPATH = ''; //域名
let ROOTPATH = 'https://sstt.ali251.langye.net'; //域名
// let ROOTPATH = 'https://sstt.115.langye.net'; //域名
switch (mode) {
case 'development':
// ROOTPATH = "https://sstt.ali251.langye.net"
ROOTPATH = "https://sstt.115.langye.net"
ROOTPATH = ROOTPATH
break;
case 'production':
ROOTPATH = "https://sstt.115.langye.net"
ROOTPATH = ROOTPATH
break;
default:
throw new Error('未配置环境');

@ -25,6 +25,8 @@ let apiAdmin = {
me: "/api/admin/auth/me",
scheduleList: "/api/admin/schedule/schedule-index",
customerList: "/api/admin/customer/get-list",
customerInfo:'/api/admin/customer/get-info',
customerSave:'/api/admin/customer/save',
saveCallback: "/api/admin/schedule-list-callbacks/save",
saveCheck: "/api/admin/schedule-list-checks/save",
callbackList: "/api/admin/schedule-list-callbacks/get-list",
@ -67,6 +69,8 @@ const install = (Vue, vm) => {
let adminMe = (data = {}) => vm.$u.post(apiAdmin.me, data);
let adminScheduleList = (data = {}) => vm.$u.get(apiAdmin.scheduleList, data);
let adminCustomerList = (data = {}) => vm.$u.get(apiAdmin.customerList, data);
let adminCustomerInfo = (params = {}) => vm.$u.get(apiAdmin.customerInfo+`/${params.id}`, params);
let adminCustomerSave = (data = {}) => vm.$u.post(apiAdmin.customerSave, data);
let adminSaveCallback = (data = {}) => vm.$u.post(apiAdmin.saveCallback, data);
let adminSaveCheck = (data = {}) => vm.$u.post(apiAdmin.saveCheck, data);
let adminCallbackList = (data = {}) => vm.$u.get(apiAdmin.callbackList, data);
@ -106,6 +110,8 @@ const install = (Vue, vm) => {
adminMe,
adminScheduleList,
adminCustomerList,
adminCustomerInfo,
adminCustomerSave,
adminSaveCallback,
adminSaveCheck,
adminCallbackList,

@ -4,16 +4,48 @@
<view class="container">
<u-form :model="form" ref="uForm" :label-width="200" :error-type="['message', 'border-bottom']">
<u-form-item label="日期" prop="date" label-position="left">
<u-input v-model="form.date" disabled type="text"></u-input>
</u-form-item>
<u-form-item label="客户" prop="customer_name" label-position="left">
<u-input v-model="form.customer_name" disabled type="text"></u-input>
</u-form-item>
<u-form-item label="护理员" required prop="nurse_name" label-position="left">
<u-input v-model="form.nurse_name" type="text" placeholder="请输入护理员"></u-input>
<u-form-item label="日期" prop="date" label-position="left">
<u-input v-model="form.date" disabled type="text"></u-input>
</u-form-item>
<u-form-item :required="item.type==='checkbox'?false:true" v-for="(item,index) in form.forms" :label="item.ask" :prop="index" label-position="top">
<u-form-item label="客户" prop="customer_name" label-position="left">
<u-input v-model="form.customer_name" disabled type="text"></u-input>
</u-form-item>
<!-- 联系方式 -->
<u-form-item label="联系方式" label-position="left">
<u-input v-model="editCustomer.phone" type="text" placeholder="请输入联系方式"></u-input>
</u-form-item>
<!-- 状态 单选组 0正常 1死亡 -->
<u-form-item label="状态" label-position="left">
<u-radio-group v-model="editCustomer.is_dead" active-color="#1479FF">
<u-radio :name="0">正常</u-radio>
<u-radio :name="1" style="margin-left: 24rpx;">死亡</u-radio>
</u-radio-group>
</u-form-item>
<!-- 默认地址 显示 default=1 的一条仅显示 address提供获取位置按钮更新/新增 -->
<u-form-item label="默认地址" label-position="left">
<view>
<view v-if="defaultCustomerAddress">
<view style="word-break: break-all;">{{ defaultCustomerAddress.address || '' }}</view>
<view style="margin-top: 12rpx; display: flex; align-items: center;">
<u-button size="mini" type="primary" @click="updateDefaultCustomerAddress"></u-button>
</view>
</view>
<view v-else>
<view style="display: flex; align-items: center;">
<u-button size="mini" type="primary" @click="createDefaultCustomerAddress"></u-button>
</view>
</view>
</view>
</u-form-item>
<view style="display: flex; align-items: center;margin-top: 20rpx;justify-content: center;">
<u-button size="medium" type="primary" style="margin-left: 12rpx;" @click="saveCustomerInfo"></u-button>
</view>
<u-form-item label="护理员" prop="nurse_name" label-position="left">
<u-input v-model="form.nurse_name" type="text" placeholder="请输入护理员"></u-input>
</u-form-item>
<u-form-item :required="item.type==='checkbox'?false:true" v-for="(item,index) in form.forms" :key="index" :label="item.ask" :prop="index" label-position="top">
<u-checkbox-group v-if="item.type==='checkbox'" ref="serve" @change="e => checkboxChange(e)">
<u-checkbox v-for="(item1, index1) in item.options" :key="index1" :name="item1.name"
v-model="item1.checked">
@ -27,12 +59,12 @@
</u-form-item>
<u-form-item label="对护理员哪些方面要求改进" prop="tip" label-position="top">
<u-input v-model="form.tip" type="textarea"></u-input>
</u-form-item>
<u-form-item label="对护理员有哪些方面肯定" prop="sure" label-position="top">
<u-input v-model="form.sure" type="textarea"></u-input>
</u-form-item>
<u-form-item label="备注" prop="remark" label-position="top">
<u-input v-model="form.remark" type="textarea"></u-input>
</u-form-item>
<u-form-item label="对护理员有哪些方面肯定" prop="sure" label-position="top">
<u-input v-model="form.sure" type="textarea"></u-input>
</u-form-item>
<u-form-item label="备注" prop="remark" label-position="top">
<u-input v-model="form.remark" type="textarea"></u-input>
</u-form-item>
<u-form-item label="回访图片">
<u-upload ref="uUpload" :custom-btn="true" :action="action" :file-list="fileList"
@ -81,7 +113,7 @@
import QQMapWX from '@/libs/qqmap-wx-jssdk.js'
import {
ROOTPATH
} from "@/common/config"
} from "@/common/config"
import moment from "@/libs/moment.min";
export default {
data() {
@ -91,8 +123,14 @@
id: "",
type: "add",
detail: {},
customerInfo: {},
editCustomer: {
phone: '',
is_dead: 0,
customer_address: []
},
form: {
customer_id: "",
customer_id: "",
customer_name:'',
date: '',
nurse_name: '',
@ -123,7 +161,7 @@
score: ''
}, {
ask: '告知客户可以提供的服务项目及本次上门服务的时长(5分)',
max: 3,
max: 5,
min: 0,
score: ''
}, {
@ -142,8 +180,8 @@
min: 0,
score: ''
}, {
ask: '工单服务项目能保质保量完成(质量标准参考入户检查表)(50分)',
max: 50,
ask: '工单服务项目能保质保量完成(质量标准参考入户检查表)(30分)',
max: 30,
min: 0,
score: ''
}, {
@ -224,15 +262,15 @@
address: "",
},
rules: {
nurse_name: [
{
required: true,
type: 'string',
message: '请填写护理员姓名',
trigger: ['change','blur']
}
],
rules: {
// nurse_name: [
// {
// required: true,
// type: 'string',
// message: '',
// trigger: ['change','blur']
// }
// ],
sign_image_id: [{
validator: (rule, value, callback) => {
if (this.vuex_sign_image || value) {
@ -263,23 +301,91 @@
deep: true, //
immediate: true //
}
},
onLoad(option) {
this.form.customer_id = option.customer_id;
this.id = option.id;
this.type = option.id ? 'edit' : 'add';
if (this.type === 'edit') {
this.getDetail()
}else{
this.form.date = moment().format('YYYY-MM-DD')
this.form.customer_name = option.customer_name?option.customer_name:''
}
},
onReady() {
this.load();
this.$refs.uForm.setRules(this.rules);
},
methods: {
onLoad(option) {
this.form.customer_id = option.customer_id;
this.id = option.id;
this.type = option.id ? 'edit' : 'add';
if (this.type === 'edit') {
this.getDetail()
}else{
this.form.date = moment().format('YYYY-MM-DD')
this.form.customer_name = option.customer_name?option.customer_name:''
}
if (this.form.customer_id) {
this.getCustomerInfo()
}
},
onReady() {
this.load();
this.$refs.uForm.setRules(this.rules);
},
computed: {
defaultCustomerAddress(){
const idx = this.editCustomer.customer_address.findIndex(i => Number(i.default) === 1)
return idx > -1 ? this.editCustomer.customer_address[idx] : null
}
},
methods: {
async getCustomerInfo(){
try{
const res = await this.$u.api.adminCustomerInfo({ id: this.form.customer_id })
this.customerInfo = res
//
this.editCustomer.phone = res.phone || ''
this.editCustomer.is_dead = Number(res.is_dead || 0)
this.editCustomer.customer_address = Array.isArray(res.customer_address) ? JSON.parse(JSON.stringify(res.customer_address)) : []
}catch(e){
console.error('adminCustomerInfo error', e)
}
},
defaultAddrIndex() {
return this.editCustomer.customer_address.findIndex(i => Number(i.default) === 1)
},
//
async updateDefaultCustomerAddress(){
try{
const res = await uni.getLocation({ type: 'gcj02', isHighAccuracy: true })
const location = Array.isArray(res) ? res[1] : res
if(!location) throw new Error('定位失败')
await new Promise((resolve, reject) => {
this.qqmapsdk.reverseGeocoder({
location: { latitude: location.latitude, longitude: location.longitude },
success: (r) => resolve(r),
fail: (err) => reject(err)
})
}).then(geo => {
const idx = this.defaultAddrIndex()
const addr = geo.result.address + (geo.result.formatted_addresses?.recommend || '')
// id
if (idx > -1) {
this.editCustomer.customer_address.splice(idx, 1)
}
// id
this.editCustomer.customer_address.push({ address: addr, lat: location.latitude, lng: location.longitude, default: 1 })
uni.showToast({ icon: 'none', title: '位置已更新' })
})
}catch(e){
uni.showToast({ icon: 'none', title: '定位失败' })
}
},
//
async createDefaultCustomerAddress(){
await this.updateDefaultCustomerAddress()
},
saveCustomerInfo(){
const payload = {
...this.customerInfo,
id: this.form.customer_id,
is_dead: this.editCustomer.is_dead,
phone: this.editCustomer.phone,
customer_address_list: this.editCustomer.customer_address
}
this.$u.api.adminCustomerSave(payload).then(()=>{
uni.showToast({ icon:'success', title:'已更新' })
this.getCustomerInfo()
})
},
showimg(url) {
if (url)
@ -338,132 +444,132 @@
let arr = e.filter(i => i?.trim())
console.log("arr", e, arr)
// this.$set(item, 'score', arr)
},
blurInput(item){
console.log("blur",item)
if(!this.validateNumberRange(item.score,item.min,item.max)){
uni.showToast({
title: "请输入正确的分值",
icon: "none"
})
item.score = 0
}
},
//
calculateTotalScore(form) {
// forms
if (!Array.isArray(this.form.forms)) {
this.form.total_score = 0;
return;
}
// checked true
const hasCheckedOption = this.form.forms.some(item => {
// item.options checked true
return Array.isArray(item.options) &&
item.options.some(option => option.checked === true);
});
// checked true 0
if (hasCheckedOption) {
this.form.total_score = 0;
this.form.forms.map(item=>item.score=0)
return;
}
//
const total = this.form.forms.reduce((sum, item) => {
// item.score 0
const score = Number(item.score) || 0;
return sum + score;
}, 0);
this.form.total_score = total;
},
//
validateNumberRange(value, min, max) {
// 1
let num;
// 使
if (typeof value === 'number') {
num = value;
}
//
else if (typeof value === 'string') {
//
const trimmed = value.trim();
// 使 parseFloat
num = parseFloat(trimmed);
// NaN
if (isNaN(num) || trimmed !== String(num)) {
return false;
}
}
// nullobject
else {
return false;
}
// 2
return num >= min && num <= max;
},
/**
* 验证表单数据
* @param {Array} forms - 表单数据数组
* @returns {Object} - 验证结果 { isValid: Boolean, errorMsg: String }
*/
validateForm(forms) {
// 1. options checked=true
const specialItem = forms.find(item =>
Array.isArray(item.options) &&
item.options.some(option => option.checked === true)
);
// 0
if (specialItem) {
forms.map(item=>item.score = 0)
return { isValid: true, totalScore: 0, errorMsg: '' };
}
// 2. min max
for (const item of forms) {
//
if (Array.isArray(item.options)) continue;
const { score, min, max } = item;
const parsedScore = parseFloat(score);
//
if (isNaN(parsedScore) || !isFinite(parsedScore)) {
return { isValid: false, totalScore: 0, errorMsg: `请输入有效的分数:${item.ask}` };
}
//
if (parsedScore < min || parsedScore > max) {
return { isValid: false, totalScore: 0, errorMsg: `分数超出范围:${item.ask} (${min}-${max}分)` };
}
}
// 3.
const totalScore = forms.reduce((sum, item) => {
//
if (Array.isArray(item.options)) return sum;
//
return sum + parseFloat(item.score);
}, 0);
return { isValid: true, totalScore, errorMsg: '' };
},
blurInput(item){
console.log("blur",item)
if(!this.validateNumberRange(item.score,item.min,item.max)){
uni.showToast({
title: "请输入正确的分值",
icon: "none"
})
item.score = 0
}
},
//
calculateTotalScore(form) {
// forms
if (!Array.isArray(this.form.forms)) {
this.form.total_score = 0;
return;
}
// checked true
const hasCheckedOption = this.form.forms.some(item => {
// item.options checked true
return Array.isArray(item.options) &&
item.options.some(option => option.checked === true);
});
// checked true 0
if (hasCheckedOption) {
this.form.total_score = 0;
this.form.forms.map(item=>item.score=0)
return;
}
//
const total = this.form.forms.reduce((sum, item) => {
// item.score 0
const score = Number(item.score) || 0;
return sum + score;
}, 0);
this.form.total_score = total;
},
//
validateNumberRange(value, min, max) {
// 1
let num;
// 使
if (typeof value === 'number') {
num = value;
}
//
else if (typeof value === 'string') {
//
const trimmed = value.trim();
// 使 parseFloat
num = parseFloat(trimmed);
// NaN
if (isNaN(num) || trimmed !== String(num)) {
return false;
}
}
// nullobject
else {
return false;
}
// 2
return num >= min && num <= max;
},
/**
* 验证表单数据
* @param {Array} forms - 表单数据数组
* @returns {Object} - 验证结果 { isValid: Boolean, errorMsg: String }
*/
validateForm(forms) {
// 1. options checked=true
const specialItem = forms.find(item =>
Array.isArray(item.options) &&
item.options.some(option => option.checked === true)
);
// 0
if (specialItem) {
forms.map(item=>item.score = 0)
return { isValid: true, totalScore: 0, errorMsg: '' };
}
// 2. min max
for (const item of forms) {
//
if (Array.isArray(item.options)) continue;
const { score, min, max } = item;
const parsedScore = parseFloat(score);
//
if (isNaN(parsedScore) || !isFinite(parsedScore)) {
return { isValid: false, totalScore: 0, errorMsg: `请输入有效的分数:${item.ask}` };
}
//
if (parsedScore < min || parsedScore > max) {
return { isValid: false, totalScore: 0, errorMsg: `分数超出范围:${item.ask} (${min}-${max}分)` };
}
}
// 3.
const totalScore = forms.reduce((sum, item) => {
//
if (Array.isArray(item.options)) return sum;
//
return sum + parseFloat(item.score);
}, 0);
return { isValid: true, totalScore, errorMsg: '' };
},
submit() {
console.log("this.form", this.form)
const res = this.validateForm(this.form.forms)
if(!res.isValid){
uni.showToast({
title:res.errorMsg,
icon:'none',
})
return
console.log("this.form", this.form)
const res = this.validateForm(this.form.forms)
if(!res.isValid){
uni.showToast({
title:res.errorMsg,
icon:'none',
})
return
}
const uploadSignImage = () => {
return new Promise((resolve, reject) => {
@ -541,13 +647,13 @@
icon: 'success',
title: '保存成功',
})
setTimeout(() => {
if(this.type==='edit'){
uni.redirectTo({
url:'/package_sub/pages/quality/qualityHistory'
})
}else{
uni.navigateBack()
setTimeout(() => {
if(this.type==='edit'){
uni.redirectTo({
url:'/package_sub/pages/quality/qualityHistory'
})
}else{
uni.navigateBack()
}
this.$u.vuex('vuex_admin_sign_image', '')
@ -570,7 +676,7 @@
this.detail = res;
for (let key in this.form) {
this.form[key] = res[key]
}
}
this.form.customer_name = res.customer.name
console.log(this.form)
this.fileList = res.files.map(i => ({

@ -46,7 +46,7 @@
</u-grid-item>
<u-grid-item v-if="isQuality" :index="'/package_sub/pages/quality/quality'" :key="5">
<u-icon :name="'list'" :size="46"></u-icon>
<view class="grid-text">质控列表</view>
<view class="grid-text">质控回访</view>
</u-grid-item>
</u-grid>
</view>

@ -6,17 +6,37 @@
<view>
<u-button :custom-style="{'margin':'24rpx 24rpx 0 24rpx'}" type="warning" @click="refresh"></u-button>
</view>
<!-- 搜索 -->
<view class="search">
<view class="input-content">
<u-input :clearable="false" :value="select.keyword" placeholder="请输入要搜索的内容" height="30" :custom-style="inputStyle"
@input="searchInput"></u-input>
<!-- 搜索同一行 -->
<view class="search-row">
<view class="search small">
<view class="input-content">
<u-input :clearable="false" :value="select.keyword" placeholder="请输入要搜索的内容" height="30" :custom-style="inputStyle"
@input="searchInput"></u-input>
</view>
<view class="icon">
<u-icon name="search" size="46" color="#ABAEBE"></u-icon>
</view>
</view>
<view class="icon">
<u-icon name="search" size="46" color="#ABAEBE"></u-icon>
<view class="search small">
<view class="input-content">
<u-input :clearable="true" :value="select.village_name" placeholder="请输入村/社区名称" height="30" :custom-style="inputStyle"
@input="villageInput"></u-input>
</view>
<view class="icon">
<u-icon name="home" size="46" color="#ABAEBE"></u-icon>
</view>
</view>
</view>
<!-- 回访/生存状态 下拉同一行 -->
<view class="drop-down" style="padding: 0 24rpx;">
<u-dropdown>
<u-dropdown-item v-model="select.has_quality_callbacks" title="回访" :options="callbackDropdownOptions" />
<u-dropdown-item v-model="select.is_dead" title="状态" :options="deadDropdownOptions" />
</u-dropdown>
</view>
<!-- 护理列表 -->
<view class="nursing-list">
<view v-if="nursingList && nursingList.length > 0">
@ -24,9 +44,9 @@
<view class="top">
<view v-if="item.quality_callbacks_count>0" @click="pageTo('/package_sub/pages/quality/qualityHistory?customer_id='+item.id)" class="time">
回访次数 <text style="color:#2979ff">{{ item.quality_callbacks_count }}</text> </view>
<view v-else class="time">回访次数 {{ 0 }} </view>
<view v-else class="time">回访次数 {{ 0 }} </view>
<view class="status">
<view class="status-text">{{ item.product_type.name }}</view>
<view class="status-text">{{ item.product_type_name }}</view>
</view>
</view>
<view class="line"></view>
@ -65,7 +85,7 @@
<view>
<u-icon name="man-add-fill" size="28" color="#1479FF"></u-icon>
</view>
<view class="text">联系人 {{ item.contact_name }}</view>
<view class="text">联系人 {{ item.contact_name?item.contact_name:'' }}</view>
</view>
</view>
<view class="sex">
@ -93,6 +113,11 @@
<u-loadmore :status="status" :load-text='loadText' @loadmore='select.page++,getList()' />
<u-back-top :scroll-top="scrollTop"></u-back-top>
<!-- 附近客户 悬浮按钮 -->
<view class="nearby-fab" @click="fetchNearby">
<text class="nearby-fab-text">附近客户</text>
</view>
</view>
</template>
@ -104,10 +129,20 @@ export default {
scrollTop: 0,
isShowCalendar: false,
inputStyle: {
width: "600rpx",
width: "100%",
fontSize: "28rpx",
fontWeight: "500"
},
callbackDropdownOptions: [
{ label: '全部', value: 'all' },
{ label: '无回访', value: 0 },
{ label: '有回访', value: 1 },
],
deadDropdownOptions: [
{ label: '全部', value: 'all' },
{ label: '正常', value: 0 },
{ label: '死亡', value: 1 },
],
optionsStatus: [{
label: '全部',
value: ''
@ -151,7 +186,12 @@ export default {
select: {
page_size: 10,
page: 1,
keyword: ''
keyword: '',
village_name: '',
has_quality_callbacks: 'all',
is_dead: 'all',
lat: '',
lng: ''
},
}
},
@ -161,7 +201,12 @@ export default {
this.select = {
page_size: 10,
page: 1,
keyword: ''
keyword: '',
village_name: '',
has_quality_callbacks: 'all',
is_dead: 'all',
lat: '',
lng: ''
}
this.nursingList = []
this.getList()
@ -175,8 +220,37 @@ export default {
this.select.page = 1
this.$u.debounce(this.getList, 1000)
},
villageInput(e) {
this.select.village_name = e
this.nursingList = []
this.select.page = 1
this.$u.debounce(this.getList, 1000)
},
async fetchNearby() {
try {
const res = await uni.getLocation({ type: 'gcj02', isHighAccuracy: true })
// uni.getLocation Promise [err, data]
const location = Array.isArray(res) ? res[1] : res
if (location && location.latitude && location.longitude) {
this.select.lat = location.latitude
this.select.lng = location.longitude
this.nursingList = []
this.select.page = 1
this.getList()
} else {
uni.showToast({ icon: 'none', title: '定位失败,请稍后重试' })
}
} catch (e) {
uni.showToast({ icon: 'none', title: '未能获取定位权限' })
}
},
async getList() {
const response = await this.$u.api.adminCustomerList(this.select)
const params = { ...this.select }
if (params.has_quality_callbacks === 'all') params.has_quality_callbacks = ''
if (params.is_dead === 'all') params.is_dead = ''
const response = await this.$u.api.adminCustomerList(params)
console.log("response",response)
let res = response.data
if (res.data.length > 0 && res.data) {
@ -221,6 +295,16 @@ export default {
}
},
watch: {
'select.has_quality_callbacks'(val) {
this.nursingList = []
this.select.page = 1
this.getList()
},
'select.is_dead'(val) {
this.nursingList = []
this.select.page = 1
this.getList()
}
},
onReachBottom() {
this.select.page++
@ -241,11 +325,11 @@ export default {
<style scoped lang="scss">
//
/deep/.u-dropdown__menu {
justify-content: space-evenly !important;
justify-content: space-between !important;
}
/deep/.u-dropdown__menu__item {
width: 224rpx !important;
width: 340rpx !important; //
flex: none !important;
height: 70rpx;
background: #FFFFFF;
@ -305,6 +389,18 @@ export default {
}
}
.search-row {
width: 710rpx;
display: flex;
justify-content: space-between;
margin: 24rpx 24rpx 0 24rpx;
}
.search.small {
width: 340rpx;
margin: 0;
}
.drop-down {
margin-top: 20rpx;
}
@ -505,4 +601,25 @@ export default {
}
}
}
.nearby-fab {
position: fixed;
right: 32rpx;
bottom: 120rpx;
width: 140rpx;
height: 140rpx;
background: #1479FF;
color: #FFFFFF;
border-radius: 70rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(20, 121, 255, 0.35);
z-index: 9;
}
.nearby-fab-text {
font-size: 26rpx;
font-weight: 600;
}
</style>

Loading…
Cancel
Save