1576 lines
40 KiB
Vue
1576 lines
40 KiB
Vue
<template>
|
||
<view class="service-list-container">
|
||
<!-- 工具条(参考「服务记录」:放在 scroll-view 外,避免顶部空白/滚动错位) -->
|
||
<view class="service-status-toolbar">
|
||
<text class="toolbar-total">共{{ serviceStatusTotal }}条</text>
|
||
<input
|
||
class="toolbar-input"
|
||
v-model="serviceStatusQuery.salesName"
|
||
placeholder="所属销售姓名"
|
||
placeholder-style="color: #b0b0b0"
|
||
confirm-type="search"
|
||
@confirm="onServiceStatusSearch"
|
||
/>
|
||
<input
|
||
class="toolbar-input"
|
||
v-model="serviceStatusQuery.customerName"
|
||
placeholder="客户姓名"
|
||
placeholder-style="color: #b0b0b0"
|
||
confirm-type="search"
|
||
@confirm="onServiceStatusSearch"
|
||
/>
|
||
<input
|
||
v-if="isAdmin"
|
||
class="toolbar-input"
|
||
v-model="serviceStatusQuery.salesPhone"
|
||
placeholder="销售电话"
|
||
placeholder-style="color: #b0b0b0"
|
||
confirm-type="search"
|
||
@confirm="onServiceStatusSearch"
|
||
/>
|
||
<view class="toolbar-actions">
|
||
<view
|
||
v-if="showReceptionEntryShortcut"
|
||
class="toolbar-btn toolbar-btn--reception"
|
||
@click="goReceptionEntry"
|
||
>
|
||
<uni-icons type="home" size="18" color="#2A68FF"></uni-icons>
|
||
<text>接待</text>
|
||
</view>
|
||
<view class="toolbar-btn toolbar-btn--refresh" @click="onServiceStatusRefresh">
|
||
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
|
||
<text>刷新</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 服务状态列表 -->
|
||
<scroll-view
|
||
class="service-list"
|
||
scroll-y
|
||
enable-flex
|
||
@scrolltolower="onServiceStatusReachBottom">
|
||
<view
|
||
class="service-card"
|
||
v-for="(item, index) in serviceStatusList"
|
||
:key="item.id || index"
|
||
@click="viewServiceDetail(item)">
|
||
<view class="card-header">
|
||
<view class="staff-info">
|
||
<view class="staff-avatar">
|
||
<text class="avatar-text">{{ item.staffName ? item.staffName.charAt(0) : '未' }}</text>
|
||
</view>
|
||
<text class="staff-name">{{ item.staffName || '未分配销售' }}</text>
|
||
</view>
|
||
<view class="service-status-indicator">
|
||
<uni-icons type="bars" size="16" color="#007AFF"></uni-icons>
|
||
<text class="service-status-indicator__text">{{ recordStateLabel }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="service-title" v-if="item.title">
|
||
<text class="service-title__text">{{ item.title }}</text>
|
||
</view>
|
||
|
||
<view class="customer-info">
|
||
<text class="customer-name">客户:{{ item.customerName || '未知客户' }}</text>
|
||
<text class="customer-phone" v-if="item.customerPhone">电话:{{ item.customerPhone }}</text>
|
||
</view>
|
||
|
||
<view class="customer-tags">
|
||
<view
|
||
class="tag-item"
|
||
:class="'tag-' + tag.color"
|
||
v-for="(tag, tagIndex) in item.tags"
|
||
:key="tagIndex">
|
||
<text>{{ tag.text }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI提醒 -->
|
||
<view class="ai-alert" v-if="item.alert" :class="item.alert.type === 'risk' ? 'alert-risk-box' : 'alert-reminder-box'">
|
||
<view class="alert-header">
|
||
<view class="alert-icon">
|
||
<view class="icon-circle"></view>
|
||
</view>
|
||
<text class="alert-title">{{ item.alert.title }}</text>
|
||
</view>
|
||
<view class="alert-content" :class="item.alert.type === 'risk' ? 'alert-risk-text' : 'alert-reminder-text'">
|
||
<text v-if="item.alert.type === 'risk'">{{ item.alert.message }}</text>
|
||
<view v-else class="alert-list">
|
||
<view class="alert-item" v-for="(msg, msgIndex) in item.alert.messages" :key="msgIndex">
|
||
<text>{{ msg }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="service-duration">
|
||
<text>{{ item.durationText }}</text>
|
||
</view>
|
||
<view class="service-actions-row">
|
||
<view
|
||
v-if="!isRecordingItem(item.id)"
|
||
class="service-action-btn service-action-btn--record"
|
||
@click.stop="startPhoneRecord(item)"
|
||
>
|
||
<text>录音</text>
|
||
</view>
|
||
<view
|
||
v-else
|
||
class="service-action-btn service-action-btn--recording"
|
||
@click.stop="stopPhoneRecordAndUpload(item)"
|
||
>
|
||
<text>停止录音</text>
|
||
</view>
|
||
<view class="service-action-btn service-action-btn--supplement" @click.stop="showSupplementDialog(item)">
|
||
<text>补录客户</text>
|
||
</view>
|
||
<view class="service-action-btn service-action-btn--upload" @click.stop="chooseAndUploadFile(item)">
|
||
<text>{{ isUploading(item.id) ? '上传中' : '上传录音' }}</text>
|
||
</view>
|
||
<view class="service-action-btn service-action-btn--finish" @click.stop="finishService(item)">
|
||
<text>结束服务</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
|
||
<text>{{ emptyListHint }}</text>
|
||
</view>
|
||
<view class="service-status-loading-more" v-if="serviceStatusLoading && serviceStatusList.length">
|
||
<text>正在加载更多...</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 补录弹窗 -->
|
||
<view class="supplement-dialog-mask" v-if="showSupplementModal" @click="closeSupplementDialog">
|
||
<view class="supplement-dialog" @click.stop>
|
||
<view class="supplement-dialog__header">
|
||
<text class="supplement-dialog__title">补录信息</text>
|
||
<view class="supplement-dialog__close" @click="closeSupplementDialog">
|
||
<uni-icons type="close" size="20" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view class="supplement-dialog__body">
|
||
<view class="supplement-form-item">
|
||
<text class="supplement-form-item__label">客户姓名</text>
|
||
<input
|
||
class="supplement-form-item__input"
|
||
v-model="supplementForm.customerName"
|
||
placeholder="请输入客户姓名"
|
||
placeholder-style="color: #9ca3af"
|
||
/>
|
||
</view>
|
||
<view class="supplement-form-item">
|
||
<text class="supplement-form-item__label">客户电话</text>
|
||
<input
|
||
class="supplement-form-item__input"
|
||
v-model="supplementForm.contact"
|
||
type="number"
|
||
placeholder="请输入客户电话"
|
||
placeholder-style="color: #9ca3af"
|
||
/>
|
||
</view>
|
||
<view class="supplement-form-item">
|
||
<text class="supplement-form-item__label">录音名</text>
|
||
<input
|
||
class="supplement-form-item__input"
|
||
v-model="supplementForm.recordingName"
|
||
placeholder="请输入录音名"
|
||
placeholder-style="color: #9ca3af"
|
||
/>
|
||
</view>
|
||
<view class="supplement-form-item">
|
||
<text class="supplement-form-item__label">客户详细地址</text>
|
||
<textarea
|
||
class="supplement-form-item__textarea"
|
||
v-model="supplementForm.remarks"
|
||
placeholder="请输入客户详细地址"
|
||
placeholder-style="color: #9ca3af"
|
||
:maxlength="500"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<view class="supplement-dialog__footer">
|
||
<view class="supplement-dialog__btn supplement-dialog__btn--cancel" @click="closeSupplementDialog">
|
||
<text>取消</text>
|
||
</view>
|
||
<view class="supplement-dialog__btn supplement-dialog__btn--save" @click="saveSupplement">
|
||
<text>保存</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { getApiUrl } from "@/common/config.js";
|
||
import { store } from "@/common/store.js";
|
||
|
||
function firstNonEmptyString(...parts) {
|
||
for (const p of parts) {
|
||
if (p === undefined || p === null) continue;
|
||
const s = String(p).trim();
|
||
if (s) return s;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
export default {
|
||
props: {
|
||
/** 卡片右侧状态文案,默认与接待页「服务中」Tab 一致 */
|
||
recordStateLabel: {
|
||
type: String,
|
||
default: '服务中',
|
||
},
|
||
emptyListHint: {
|
||
type: String,
|
||
default: '暂无服务中记录',
|
||
},
|
||
/** 是否在工具栏显示进入完整「接待」页的快捷入口 */
|
||
showReceptionEntryShortcut: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
/** 卡片右侧是否显示状态横杠图标与 recordStateLabel 文案 */
|
||
showRecordStateIndicator: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
serviceStatusLoading: false,
|
||
serviceStatusTotal: 0,
|
||
serviceStatusPage: {
|
||
current: 1,
|
||
size: 10
|
||
},
|
||
serviceStatusQuery: {
|
||
salesName: '',
|
||
customerName: '',
|
||
salesPhone: ''
|
||
},
|
||
isAdmin: false,
|
||
currentUserPhone: '',
|
||
serviceStatusList: [],
|
||
// 补录相关
|
||
showSupplementModal: false,
|
||
currentSupplementItem: null,
|
||
uploadingIds: [],
|
||
supplementForm: {
|
||
customerName: "",
|
||
contact: "",
|
||
recordingName: "",
|
||
remarks: ""
|
||
},
|
||
recorderSupported: false,
|
||
recorderManager: null,
|
||
recordingServiceId: null,
|
||
recordingContext: null
|
||
}
|
||
},
|
||
mounted() {
|
||
// 检查用户角色
|
||
this.checkUserRole();
|
||
// 获取当前用户手机号
|
||
this.loadCurrentUserPhone();
|
||
this.initServiceRecorder();
|
||
this.fetchServiceStatusList();
|
||
},
|
||
beforeUnmount() {
|
||
if (this.recorderManager && this.recordingServiceId) {
|
||
try {
|
||
this.recorderManager.stop();
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
initServiceRecorder() {
|
||
this.recorderSupported = typeof uni.getRecorderManager === 'function';
|
||
if (!this.recorderSupported) {
|
||
return;
|
||
}
|
||
this.recorderManager = uni.getRecorderManager();
|
||
// 用户点击「停止」后由系统回调此处,再将临时录音文件上传到 /api/audio/upload
|
||
this.recorderManager.onStop((res) => {
|
||
const ctx = this.recordingContext;
|
||
this.recordingServiceId = null;
|
||
this.recordingContext = null;
|
||
const tempFilePath = res.tempFilePath || '';
|
||
if (!ctx || !ctx.id) {
|
||
return;
|
||
}
|
||
if (!tempFilePath) {
|
||
uni.showToast({ title: '未获取到录音文件', icon: 'none' });
|
||
return;
|
||
}
|
||
const name = `service_record_${ctx.id}_${Date.now()}.mp3`;
|
||
this.uploadFileForRecord(ctx, { path: tempFilePath, name });
|
||
});
|
||
this.recorderManager.onError(() => {
|
||
this.recordingServiceId = null;
|
||
this.recordingContext = null;
|
||
uni.showToast({ title: '录音出错', icon: 'none' });
|
||
});
|
||
},
|
||
isRecordingItem(recordId) {
|
||
if (!recordId || !this.recordingServiceId) {
|
||
return false;
|
||
}
|
||
return String(recordId) === this.recordingServiceId;
|
||
},
|
||
startPhoneRecord(item) {
|
||
if (!this.recorderSupported || !this.recorderManager) {
|
||
uni.showToast({
|
||
title: '当前环境不支持录音,请使用微信小程序或 App',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
if (!item || !item.id) {
|
||
uni.showToast({
|
||
title: '无法获取服务记录ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
if (this.recordingServiceId) {
|
||
uni.showToast({ title: '请先停止当前录音', icon: 'none' });
|
||
return;
|
||
}
|
||
if (this.isUploading(item.id)) {
|
||
uni.showToast({ title: '正在上传,请稍候', icon: 'none' });
|
||
return;
|
||
}
|
||
this.recordingContext = { ...item };
|
||
this.recordingServiceId = String(item.id);
|
||
try {
|
||
this.recorderManager.start({
|
||
duration: 600000,
|
||
sampleRate: 44100,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 96000,
|
||
format: 'mp3'
|
||
});
|
||
} catch (e) {
|
||
this.recordingServiceId = null;
|
||
this.recordingContext = null;
|
||
uni.showToast({ title: '无法开始录音', icon: 'none' });
|
||
}
|
||
},
|
||
/** 结束录音;onStop 回调中会调用 uploadFileForRecord 上传至后端 */
|
||
stopPhoneRecordAndUpload(item) {
|
||
if (!this.recorderManager || !item?.id) {
|
||
return;
|
||
}
|
||
if (String(item.id) !== this.recordingServiceId) {
|
||
return;
|
||
}
|
||
this.recorderManager.stop();
|
||
},
|
||
/**
|
||
* 检查用户角色,判断是否包含admin
|
||
* 只要角色中包含admin字符串(如admin_furniture),就视为admin角色
|
||
*/
|
||
checkUserRole() {
|
||
try {
|
||
const normalize = (val) =>
|
||
typeof val === 'string'
|
||
? val
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean)
|
||
: [];
|
||
const storedRole = uni.getStorageSync('backend-role-name') || '';
|
||
const loginResp = uni.getStorageSync('backend-login-response') || {};
|
||
const respRole = loginResp.roleName || '';
|
||
const roles = [...normalize(storedRole), ...normalize(respRole)];
|
||
// 检查角色数组中是否有任何角色包含 'admin' 字符串
|
||
this.isAdmin = roles.some(role => role.includes('admin'));
|
||
console.log('用户角色检查:', { storedRole, respRole, roles, isAdmin: this.isAdmin });
|
||
} catch (e) {
|
||
console.error('检查用户角色失败:', e);
|
||
this.isAdmin = false;
|
||
}
|
||
},
|
||
/**
|
||
* 加载当前用户手机号或登录账户
|
||
* 优先使用手机号,如果没有手机号则使用登录账户
|
||
*/
|
||
loadCurrentUserPhone() {
|
||
try {
|
||
const loginResponse = uni.getStorageSync('backend-login-response') || {};
|
||
// 优先使用手机号,如果没有手机号则使用登录账户(userName)
|
||
if (loginResponse.phone) {
|
||
this.currentUserPhone = loginResponse.phone;
|
||
} else if (loginResponse.userName) {
|
||
this.currentUserPhone = loginResponse.userName;
|
||
} else {
|
||
this.currentUserPhone = '';
|
||
}
|
||
} catch (e) {
|
||
console.error('加载当前用户手机号或登录账户失败:', e);
|
||
this.currentUserPhone = '';
|
||
}
|
||
},
|
||
onServiceStatusSearch() {
|
||
this.serviceStatusPage.current = 1;
|
||
this.fetchServiceStatusList({ force: true });
|
||
},
|
||
onServiceStatusRefresh() {
|
||
this.serviceStatusPage.current = 1;
|
||
this.fetchServiceStatusList({ force: true });
|
||
},
|
||
goReceptionEntry() {
|
||
const url = '/pages-subpackage/furniture_reception/furniture_reception_entry?tab=reception';
|
||
uni.navigateTo({
|
||
url,
|
||
fail: () => {
|
||
uni.showToast({ title: '跳转接待失败', icon: 'none' });
|
||
},
|
||
});
|
||
},
|
||
onServiceStatusReachBottom() {
|
||
// 已在加载中或全部加载完成则不再触发
|
||
if (this.serviceStatusLoading) return;
|
||
if (this.serviceStatusList.length >= this.serviceStatusTotal) return;
|
||
this.serviceStatusPage.current += 1;
|
||
this.fetchServiceStatusList();
|
||
},
|
||
async fetchServiceStatusList({ force = false } = {}) {
|
||
if (this.serviceStatusLoading && !force) {
|
||
return;
|
||
}
|
||
this.serviceStatusLoading = true;
|
||
try {
|
||
// 构建查询参数对象
|
||
const queryParams = {
|
||
current: this.serviceStatusPage.current,
|
||
size: this.serviceStatusPage.size,
|
||
serviceStatus: '服务中' // 默认搜索条件
|
||
};
|
||
const trimmedSalesName = this.serviceStatusQuery?.salesName?.trim();
|
||
const trimmedCustomerName = this.serviceStatusQuery?.customerName?.trim();
|
||
if (trimmedSalesName) {
|
||
queryParams.salesName = trimmedSalesName;
|
||
}
|
||
if (trimmedCustomerName) {
|
||
queryParams.customerName = trimmedCustomerName;
|
||
}
|
||
|
||
// 根据角色处理销售电话查询条件
|
||
if (this.isAdmin) {
|
||
// 如果是admin,使用搜索框中的销售电话
|
||
const trimmedSalesPhone = this.serviceStatusQuery?.salesPhone?.trim();
|
||
if (trimmedSalesPhone) {
|
||
queryParams.salesPhone = trimmedSalesPhone;
|
||
}
|
||
} else {
|
||
// 如果不是admin,默认添加当前登录人的手机号或登录账户
|
||
if (this.currentUserPhone) {
|
||
queryParams.salesPhone = this.currentUserPhone;
|
||
}
|
||
}
|
||
|
||
// 确保 serviceStatus 始终存在
|
||
queryParams.serviceStatus = '服务中';
|
||
|
||
// 将参数转换为 URL 查询字符串(因为后端使用 @RequestParam)
|
||
const queryString = Object.keys(queryParams)
|
||
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
|
||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
|
||
.join('&');
|
||
|
||
// 调试:打印请求参数
|
||
console.log('服务状态查询参数:', JSON.stringify(queryParams));
|
||
console.log('查询字符串:', queryString);
|
||
|
||
// 后端使用 @RequestParam,参数需要作为 URL 查询参数传递
|
||
const baseUrl = getApiUrl('/api/audioManagement/list');
|
||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
const res = await uni.request({
|
||
url,
|
||
method: 'POST',
|
||
data: {}, // POST 请求体为空,参数都在 URL 查询字符串中
|
||
header: headers,
|
||
timeout: 30000 // 增加超时时间到30秒
|
||
});
|
||
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
const records = Array.isArray(res.data.data) ? res.data.data : [];
|
||
|
||
if (records.length === 0) {
|
||
this.serviceStatusList = [];
|
||
this.serviceStatusTotal = 0;
|
||
return;
|
||
}
|
||
|
||
const mapped = records
|
||
.map(item => this.buildServiceStatusItem(item))
|
||
.filter(item => item !== null); // 过滤掉无效的数据
|
||
|
||
// 第一页或强制刷新时重置列表,否则追加
|
||
if (this.serviceStatusPage.current === 1 || force) {
|
||
// 使用 Vue 3 的响应式更新方式
|
||
this.serviceStatusList = [...mapped];
|
||
} else {
|
||
this.serviceStatusList = [...this.serviceStatusList, ...mapped];
|
||
}
|
||
this.serviceStatusTotal = Number(res.data.total) || 0;
|
||
} else {
|
||
this.serviceStatusList = [];
|
||
this.serviceStatusTotal = 0;
|
||
uni.showToast({
|
||
title: res.data?.message || '获取服务中列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('获取服务中列表失败:', error);
|
||
// 根据错误类型提供更详细的错误信息
|
||
let errorMessage = '获取服务状态失败,请稍后重试';
|
||
if (error.errMsg) {
|
||
if (error.errMsg.includes('timeout')) {
|
||
errorMessage = '请求超时,请检查网络连接后重试';
|
||
} else if (error.errMsg.includes('fail')) {
|
||
errorMessage = '网络请求失败,请检查网络连接';
|
||
}
|
||
}
|
||
uni.showToast({
|
||
title: errorMessage,
|
||
icon: 'none',
|
||
duration: 3000
|
||
});
|
||
} finally {
|
||
this.serviceStatusLoading = false;
|
||
}
|
||
},
|
||
buildServiceStatusItem(record = {}) {
|
||
if (!record || typeof record !== 'object') {
|
||
return null;
|
||
}
|
||
|
||
const formattedCreateTime = this.formatDateTime(record.createTime);
|
||
const tags = [];
|
||
if (record.recordingName) {
|
||
tags.push({ text: `录音:${record.recordingName}`, color: 'blue' });
|
||
}
|
||
if (record.intentionLevel) {
|
||
tags.push({ text: `意向:${record.intentionLevel}`, color: 'orange' });
|
||
}
|
||
if (record.projectName) {
|
||
tags.push({ text: `项目:${record.projectName}`, color: 'blue' });
|
||
}
|
||
|
||
const title =
|
||
firstNonEmptyString(record.recordingName) ||
|
||
(firstNonEmptyString(record.customerName) ? `${String(record.customerName).trim()}的接待记录` : '') ||
|
||
'';
|
||
|
||
return {
|
||
id: record.id || '',
|
||
staffName: record.salesName || '未分配销售',
|
||
status: record.syncStatus || '服务中',
|
||
customerName: record.customerName || '',
|
||
customerPhone: record.customerPhone || '',
|
||
customerId: record.customerId || '',
|
||
recordingName: record.recordingName || '',
|
||
title,
|
||
remarks: record.remarks || '',
|
||
tags,
|
||
durationText: formattedCreateTime ? `开始时间:${formattedCreateTime}` : '暂无开始时间'
|
||
};
|
||
},
|
||
formatDateTime(value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '';
|
||
}
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
},
|
||
viewServiceDetail(item) {
|
||
// 查看服务详情
|
||
console.log('查看服务详情', item);
|
||
},
|
||
isUploading(recordId) {
|
||
if (!recordId) return false;
|
||
return this.uploadingIds.includes(String(recordId));
|
||
},
|
||
/** 仅允许扩展名为 .mp3 的文件(不区分大小写) */
|
||
isMp3UploadFile(selected) {
|
||
if (!selected || !selected.path) return false;
|
||
const pickBase = (s) => {
|
||
if (!s || typeof s !== 'string') return '';
|
||
const seg = s.split('/').pop() || s.split('\\').pop() || s;
|
||
return seg.trim();
|
||
};
|
||
const bases = [pickBase(selected.name), pickBase(selected.path)].filter(Boolean);
|
||
for (const base of bases) {
|
||
if (base.toLowerCase().endsWith('.mp3')) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
async chooseAndUploadFile(item) {
|
||
if (!item || !item.id) {
|
||
uni.showToast({
|
||
title: '无法获取服务记录ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
if (this.isUploading(item.id)) {
|
||
return;
|
||
}
|
||
const selectedFile = await this.selectUploadFile();
|
||
if (!selectedFile || !selectedFile.path) {
|
||
return;
|
||
}
|
||
await this.uploadFileForRecord(item, selectedFile);
|
||
},
|
||
selectUploadFile() {
|
||
return new Promise((resolve) => {
|
||
const normalizeSelected = (res) => {
|
||
const tempFiles = Array.isArray(res?.tempFiles) ? res.tempFiles : [];
|
||
if (!tempFiles.length) return null;
|
||
const file = tempFiles[0] || {};
|
||
const filePath = file.path || file.tempFilePath || file.url || '';
|
||
const fileName = file.name || filePath.split('/').pop() || `service_file_${Date.now()}`;
|
||
if (!filePath) return null;
|
||
return {
|
||
path: filePath,
|
||
name: fileName
|
||
};
|
||
};
|
||
const onSuccess = (res) => {
|
||
const selected = normalizeSelected(res);
|
||
if (!selected) {
|
||
uni.showToast({
|
||
title: '未选择有效文件',
|
||
icon: 'none'
|
||
});
|
||
resolve(null);
|
||
return;
|
||
}
|
||
if (!this.isMp3UploadFile(selected)) {
|
||
uni.showToast({
|
||
title: '仅支持上传 MP3 文件',
|
||
icon: 'none'
|
||
});
|
||
resolve(null);
|
||
return;
|
||
}
|
||
resolve(selected);
|
||
};
|
||
const onFail = (err) => {
|
||
const errMsg = err?.errMsg || '';
|
||
if (errMsg.includes('cancel')) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
console.error('选择文件失败:', err);
|
||
uni.showToast({
|
||
title: '选择文件失败',
|
||
icon: 'none'
|
||
});
|
||
resolve(null);
|
||
};
|
||
if (typeof uni.chooseMessageFile === 'function') {
|
||
uni.chooseMessageFile({
|
||
count: 1,
|
||
type: 'file',
|
||
success: onSuccess,
|
||
fail: onFail
|
||
});
|
||
return;
|
||
}
|
||
if (typeof uni.chooseFile === 'function') {
|
||
uni.chooseFile({
|
||
count: 1,
|
||
extension: ['.mp3'],
|
||
success: onSuccess,
|
||
fail: onFail
|
||
});
|
||
return;
|
||
}
|
||
uni.chooseImage({
|
||
count: 1,
|
||
success: onSuccess,
|
||
fail: onFail
|
||
});
|
||
});
|
||
},
|
||
getAuthHeaders() {
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
const headers = {};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
return headers;
|
||
},
|
||
async uploadFileForRecord(item, selectedFile) {
|
||
const recordId = String(item.id);
|
||
this.uploadingIds = [...this.uploadingIds, recordId];
|
||
try {
|
||
uni.showLoading({
|
||
title: '上传中...'
|
||
});
|
||
const uploadRes = await uni.uploadFile({
|
||
url: getApiUrl('/api/audio/upload'),
|
||
filePath: selectedFile.path,
|
||
name: 'file',
|
||
formData: {
|
||
id: item.id,
|
||
audioId: item.id,
|
||
customerId: item.customerId || '',
|
||
customerName: item.customerName || '',
|
||
fileName: selectedFile.name || `service_file_${Date.now()}`
|
||
},
|
||
header: this.getAuthHeaders(),
|
||
timeout: 60000
|
||
});
|
||
let result = {};
|
||
try {
|
||
result = typeof uploadRes?.data === 'string' ? JSON.parse(uploadRes.data) : (uploadRes?.data || {});
|
||
} catch (e) {
|
||
result = uploadRes?.data || {};
|
||
}
|
||
if (uploadRes?.statusCode === 200 && result?.success !== false) {
|
||
uni.showToast({
|
||
title: '上传成功',
|
||
icon: 'success'
|
||
});
|
||
this.serviceStatusPage.current = 1;
|
||
this.fetchServiceStatusList({ force: true });
|
||
return;
|
||
}
|
||
uni.showToast({
|
||
title: result?.message || '上传失败',
|
||
icon: 'none'
|
||
});
|
||
} catch (error) {
|
||
console.error('上传文件失败:', error);
|
||
uni.showToast({
|
||
title: error?.errMsg || error?.message || '上传失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
uni.hideLoading();
|
||
this.uploadingIds = this.uploadingIds.filter(id => id !== recordId);
|
||
}
|
||
},
|
||
async finishService(item) {
|
||
if (!item || !item.id) {
|
||
uni.showToast({
|
||
title: '无法获取服务记录ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
uni.showModal({
|
||
title: '确认结束',
|
||
content: `确定要结束这条服务记录吗?`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
uni.showLoading({
|
||
title: '结束中...'
|
||
});
|
||
|
||
const url = getApiUrl('/api/audioManagement/finishServiceById');
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
// 构建请求体
|
||
const requestData = {
|
||
id: item.id
|
||
};
|
||
|
||
const finishRes = await uni.request({
|
||
url: url,
|
||
method: 'POST',
|
||
data: requestData,
|
||
header: headers,
|
||
timeout: 30000
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
if (finishRes.statusCode === 200 && finishRes.data && finishRes.data.success) {
|
||
const rawSuccessMsg = finishRes.data?.message || '结束服务成功';
|
||
const successTitle = rawSuccessMsg
|
||
.replace(/[A-Za-z]+/g, '')
|
||
.split(/\n/)
|
||
.map((line) => line.trim())
|
||
.filter(Boolean)
|
||
.join('\n') || '结束服务成功';
|
||
uni.showToast({
|
||
title: successTitle,
|
||
icon: 'success'
|
||
});
|
||
// 结束成功后刷新列表
|
||
this.serviceStatusPage.current = 1;
|
||
this.fetchServiceStatusList({ force: true });
|
||
} else {
|
||
uni.showToast({
|
||
title: finishRes.data?.message || '结束服务失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('结束服务失败:', error);
|
||
// 根据错误类型提供更详细的错误信息
|
||
let errorMessage = '结束服务失败,请重试';
|
||
if (error.errMsg) {
|
||
if (error.errMsg.includes('timeout')) {
|
||
errorMessage = '请求超时,请检查网络连接后重试';
|
||
} else if (error.errMsg.includes('fail')) {
|
||
errorMessage = '网络请求失败,请检查网络连接';
|
||
}
|
||
}
|
||
uni.showToast({
|
||
title: errorMessage,
|
||
icon: 'none',
|
||
duration: 3000
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
},
|
||
showSupplementDialog(item) {
|
||
this.currentSupplementItem = item;
|
||
// 初始化表单数据,如果有现有数据则填充
|
||
this.supplementForm = {
|
||
customerName: item.customerName || "",
|
||
contact: item.customerPhone || "",
|
||
recordingName: item.recordingName || "",
|
||
remarks: item.remarks || ""
|
||
};
|
||
this.showSupplementModal = true;
|
||
},
|
||
closeSupplementDialog() {
|
||
this.showSupplementModal = false;
|
||
this.currentSupplementItem = null;
|
||
this.supplementForm = {
|
||
customerName: "",
|
||
contact: "",
|
||
recordingName: "",
|
||
remarks: ""
|
||
};
|
||
},
|
||
async saveSupplement() {
|
||
// 验证电话格式(如果填写了电话)
|
||
const trimmedContact = this.supplementForm.contact?.trim();
|
||
if (trimmedContact && !this.isValidPhoneNumber(trimmedContact)) {
|
||
uni.showToast({
|
||
title: "请输入有效的客户电话",
|
||
icon: "none"
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.currentSupplementItem || !this.currentSupplementItem.id) {
|
||
uni.showToast({
|
||
title: '无法获取服务记录ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: "保存中..."
|
||
});
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
let loginResponse = {};
|
||
try {
|
||
loginResponse = uni.getStorageSync("backend-login-response") || {};
|
||
} catch (e) {
|
||
console.error("读取登录信息失败:", e);
|
||
}
|
||
const ui = store.userInfo || {};
|
||
// 与接待页、列表筛选一致:销售电话优先手机号,否则登录账号
|
||
const salesPhone = firstNonEmptyString(
|
||
loginResponse.phone,
|
||
loginResponse.userName,
|
||
ui.username
|
||
);
|
||
// 姓名:优先后端姓名类字段,再退回昵称、登录名
|
||
const salesName = firstNonEmptyString(
|
||
loginResponse.realName,
|
||
loginResponse.name,
|
||
loginResponse.nickName,
|
||
ui.nickname,
|
||
loginResponse.userName,
|
||
ui.username
|
||
);
|
||
|
||
// 构建请求参数(必须包含id)
|
||
const params = {
|
||
id: this.currentSupplementItem.id,
|
||
customerId: this.currentSupplementItem.customerId || "",
|
||
customerName: this.supplementForm.customerName?.trim() || "",
|
||
customerPhone: trimmedContact || "",
|
||
recordingName: this.supplementForm.recordingName?.trim() || "",
|
||
remarks: this.supplementForm.remarks?.trim() || "",
|
||
salesPhone: salesPhone,
|
||
salesName: salesName
|
||
};
|
||
|
||
// 调用补录接口
|
||
const res = await uni.request({
|
||
url: getApiUrl('/api/audioManagement/updateForCustomerInfo'),
|
||
method: "PUT",
|
||
data: params,
|
||
header: headers,
|
||
timeout: 30000
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
uni.showToast({
|
||
title: "补录成功",
|
||
icon: "success"
|
||
});
|
||
// 关闭弹窗
|
||
this.closeSupplementDialog();
|
||
// 刷新列表
|
||
this.serviceStatusPage.current = 1;
|
||
this.fetchServiceStatusList({ force: true });
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data?.message || "补录失败",
|
||
icon: "none"
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error("补录失败:", error);
|
||
let errorMessage = '补录失败,请重试';
|
||
if (error.errMsg) {
|
||
if (error.errMsg.includes('timeout')) {
|
||
errorMessage = '请求超时,请检查网络连接后重试';
|
||
} else if (error.errMsg.includes('fail')) {
|
||
errorMessage = '网络请求失败,请检查网络连接';
|
||
}
|
||
}
|
||
uni.showToast({
|
||
title: errorMessage,
|
||
icon: 'none',
|
||
duration: 3000
|
||
});
|
||
}
|
||
},
|
||
isValidPhoneNumber(phone) {
|
||
const normalizedPhone = phone?.trim();
|
||
if (!normalizedPhone) {
|
||
return false;
|
||
}
|
||
return /^1[3-9]\d{9}$/.test(normalizedPhone);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 容器布局(参考「服务记录」组件):工具条固定在上,列表滚动占满剩余高度 */
|
||
.service-list-container {
|
||
height: 100%;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #F5F5F5;
|
||
/* 只保留左右 padding,让 toolbar 紧贴 tab,下方不再出现大空白 */
|
||
padding: 0 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-status-toolbar {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 12rpx 8rpx;
|
||
margin-bottom: 24rpx;
|
||
background-color: #F8F8FA;
|
||
border-radius: 8rpx;
|
||
border: 1px solid #EFEFF2;
|
||
overflow-x: auto;
|
||
min-height: 64rpx;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.service-list {
|
||
flex: 1;
|
||
background-color: transparent;
|
||
box-sizing: border-box;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.toolbar-total {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
margin-right: 8rpx;
|
||
line-height: 1.2;
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
.toolbar-input {
|
||
flex: 1;
|
||
min-width: 120rpx;
|
||
max-width: 200rpx;
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
background-color: #F5F6FA;
|
||
border-radius: 8rpx;
|
||
padding: 0 12rpx;
|
||
font-size: 24rpx;
|
||
border: 1px solid transparent;
|
||
box-sizing: border-box;
|
||
flex-shrink: 1;
|
||
}
|
||
|
||
.toolbar-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
padding: 0 12rpx;
|
||
height: 56rpx;
|
||
min-width: 56rpx;
|
||
color: #2A68FF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
line-height: 1;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-card {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background-color: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
overflow: visible;
|
||
position: relative;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.service-title {
|
||
margin-top: 16rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.service-title__text {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
display: block;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.staff-info {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.staff-avatar {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
background-color: #2196F3;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 16rpx;
|
||
margin-left: 0;
|
||
margin-top: 0;
|
||
margin-bottom: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-text {
|
||
font-size: 32rpx;
|
||
color: #FFFFFF;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.staff-name {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.service-status-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.service-status-indicator__text {
|
||
font-size: 26rpx;
|
||
color: #007AFF;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.service-actions-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.service-action-btn {
|
||
padding: 8rpx 18rpx;
|
||
border-radius: 10rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.2;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid transparent;
|
||
background-color: #F8FAFF;
|
||
}
|
||
|
||
.service-action-btn--record {
|
||
color: #007AFF;
|
||
border-color: rgba(0, 122, 255, 0.22);
|
||
}
|
||
|
||
.service-action-btn--recording {
|
||
color: #FF3B30;
|
||
border-color: rgba(255, 59, 48, 0.25);
|
||
}
|
||
|
||
.service-action-btn--supplement {
|
||
color: #10B981;
|
||
border-color: rgba(16, 185, 129, 0.25);
|
||
}
|
||
|
||
.service-action-btn--upload {
|
||
color: #7C3AED;
|
||
border-color: rgba(124, 58, 237, 0.25);
|
||
}
|
||
|
||
.service-action-btn--finish {
|
||
color: #EF4444;
|
||
border-color: rgba(239, 68, 68, 0.28);
|
||
background-color: #FFF7F7;
|
||
}
|
||
|
||
.customer-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20rpx;
|
||
gap: 12rpx;
|
||
box-sizing: border-box;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
padding: 0;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.customer-info {
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
box-sizing: border-box;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
padding: 0;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.customer-name,
|
||
.customer-phone {
|
||
font-size: 28rpx;
|
||
color: #555;
|
||
}
|
||
|
||
.tag-item {
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
box-sizing: border-box;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.tag-blue {
|
||
background-color: #E3F2FD;
|
||
}
|
||
|
||
.tag-blue text {
|
||
font-size: 24rpx;
|
||
color: #1976D2;
|
||
}
|
||
|
||
.tag-orange {
|
||
background-color: #FFF3E0;
|
||
}
|
||
|
||
.tag-orange text {
|
||
font-size: 24rpx;
|
||
color: #F57C00;
|
||
}
|
||
|
||
/* AI提醒 */
|
||
.ai-alert {
|
||
border-radius: 8rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.alert-risk-box {
|
||
background-color: #FFF5F5;
|
||
}
|
||
|
||
.alert-reminder-box {
|
||
background-color: #F5F5F5;
|
||
}
|
||
|
||
.alert-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.alert-icon {
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.icon-circle {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
border: 2rpx solid #333;
|
||
border-radius: 50%;
|
||
position: relative;
|
||
}
|
||
|
||
.icon-circle::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
background-color: #333;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.alert-title {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.alert-content {
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.alert-risk-text {
|
||
color: #FF5722;
|
||
}
|
||
|
||
.alert-reminder-text {
|
||
color: #666;
|
||
}
|
||
|
||
.alert-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.alert-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.alert-item::before {
|
||
content: '•';
|
||
margin-right: 8rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.alert-item text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.service-duration {
|
||
padding-top: 16rpx;
|
||
border-top: 1px solid #F0F0F0;
|
||
}
|
||
|
||
.service-duration text {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.service-status-empty {
|
||
padding: 48rpx 0;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.service-status-loading-more {
|
||
padding: 48rpx 0;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* 补录弹窗样式 */
|
||
.supplement-dialog-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.supplement-dialog {
|
||
width: 640rpx;
|
||
max-height: 80vh;
|
||
background-color: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.supplement-dialog__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 32rpx 32rpx 24rpx 32rpx;
|
||
border-bottom: 1px solid #F0F0F0;
|
||
}
|
||
|
||
.supplement-dialog__title {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.supplement-dialog__close {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.supplement-dialog__body {
|
||
flex: 1;
|
||
padding: 32rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.supplement-form-item {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.supplement-form-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.supplement-form-item__label {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.supplement-form-item__input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background-color: #f9fafb;
|
||
border-radius: 12rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.supplement-form-item__textarea {
|
||
width: 100%;
|
||
min-height: 160rpx;
|
||
background-color: #f9fafb;
|
||
border-radius: 12rpx;
|
||
padding: 24rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
box-sizing: border-box;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.supplement-dialog__footer {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
padding: 24rpx 32rpx 32rpx 32rpx;
|
||
border-top: 1px solid #F0F0F0;
|
||
}
|
||
|
||
.supplement-dialog__btn {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.supplement-dialog__btn--cancel {
|
||
background-color: #f3f4f6;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.supplement-dialog__btn--save {
|
||
background-color: #4C8DFF;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.supplement-dialog__btn:active {
|
||
opacity: 0.7;
|
||
}
|
||
</style>
|