diff --git a/api/upload.js b/api/upload.js index e6ef622..c76e9e0 100644 --- a/api/upload.js +++ b/api/upload.js @@ -28,26 +28,26 @@ function getOssSignatureByPrefix(prefix, headerKey) { }); } -function registerOssFileByPrefix(prefix, headerKey, url) { +function registerOssFileByPrefix(prefix, headerKey, url, fileSize = 0) { return req.request({ url: `${prefix}register-oss-file`, method: 'POST', - data: { url, source: 2 }, + data: { url, source: 2, file_size: fileSize }, header: { [headerKey]: '1', }, }); } -function registerBusinessOssFile(url) { +function registerBusinessOssFile(url, fileSize = 0) { const mode = uni.getStorageSync('loginMode'); if (mode === 'platform_admin') { - return registerOssFileByPrefix(PLATFORM_PREFIX, '_platformAdmin', url); + return registerOssFileByPrefix(PLATFORM_PREFIX, '_platformAdmin', url, fileSize); } if (mode === 'clinic_admin') { - return registerOssFileByPrefix(CLINIC_ADMIN_PREFIX, '_clinicAdmin', url); + return registerOssFileByPrefix(CLINIC_ADMIN_PREFIX, '_clinicAdmin', url, fileSize); } - return registerOssFileByPrefix(SALESPERSON_PREFIX, '_salesperson', url); + return registerOssFileByPrefix(SALESPERSON_PREFIX, '_salesperson', url, fileSize); } export { registerBusinessOssFile }; diff --git a/common/js/oss-upload.js b/common/js/oss-upload.js index 4460a4d..0a6d9f9 100644 --- a/common/js/oss-upload.js +++ b/common/js/oss-upload.js @@ -48,7 +48,7 @@ export async function getOssSignatureFromBackend() { * @returns {Promise<{ url: string, objectName: string }>} */ export async function uploadToOss(options) { - const { filePath, onProgress } = options || {}; + const { filePath, onProgress, fileSize = 0 } = options || {}; if (!filePath) { throw new Error('文件路径为空'); } @@ -76,7 +76,7 @@ export async function uploadToOss(options) { if (code >= 200 && code < 300 || code === 204) { const host = String(signature.host).replace(/\/$/, ''); const url = `${host}/${objectName}`; - registerBusinessOssFile(url).catch((err) => { + registerBusinessOssFile(url, fileSize).catch((err) => { console.warn('OSS 文件登记失败:', err); }); resolve({ diff --git a/subPackages/sub_salesperson/common/imageGalleryBridge.js b/subPackages/sub_salesperson/common/imageGalleryBridge.js new file mode 100644 index 0000000..1308d75 --- /dev/null +++ b/subPackages/sub_salesperson/common/imageGalleryBridge.js @@ -0,0 +1,22 @@ +let pending = null + +export function requestGalleryPick() { + return new Promise((resolve, reject) => { + pending = { resolve, reject } + uni.$emit('salesperson:open-gallery') + }) +} + +export function resolveGalleryPick(url) { + if (pending) { + pending.resolve(url) + pending = null + } +} + +export function cancelGalleryPick() { + if (pending) { + pending.reject(new Error('cancel')) + pending = null + } +} diff --git a/subPackages/sub_salesperson/common/inputFormHelpers.js b/subPackages/sub_salesperson/common/inputFormHelpers.js index 6291dcc..6b3e3ea 100644 --- a/subPackages/sub_salesperson/common/inputFormHelpers.js +++ b/subPackages/sub_salesperson/common/inputFormHelpers.js @@ -1,45 +1,26 @@ import { prepareImagePath } from '@/common/js/image-compress.js' import { uploadToOss } from '@/common/js/oss-upload.js' +import { requestGalleryPick } from '@/subPackages/sub_salesperson/common/imageGalleryBridge.js' -/** - * 选择并上传图片(压缩 + OSS 直传) - * @param {{ count?: number }} options count>1 时依次上传多张,返回 url 数组 - */ -export function pickAndUploadImage(options = {}) { - const count = Math.min(Math.max(options.count || 1, 1), 9) +function chooseImageFromDevice(count = 1) { return new Promise((resolve, reject) => { - uni.chooseImage({ - count, - sizeType: ['original', 'compressed'], - sourceType: ['album', 'camera'], - success: async (res) => { - uni.showLoading({ title: '上传中...', mask: true }) - try { - const files = res.tempFiles || [] - const paths = files.length - ? files.map((f) => f.path) - : (res.tempFilePaths || []) - const urls = [] - for (const path of paths) { - const prepared = await prepareImagePath({ - path, - size: files.find((f) => f.path === path)?.size, - }) - if (!prepared) continue - const result = await uploadToOss({ filePath: prepared.path }) - urls.push(result.url) - } - uni.hideLoading() - if (!urls.length) { - reject(new Error('上传失败')) - return - } - resolve(count === 1 ? urls[0] : urls) - } catch (e) { - uni.hideLoading() - uni.showToast({ title: e.message || '上传失败', icon: 'none' }) - reject(e) - } + uni.showActionSheet({ + itemList: ['拍照', '从相册选择'], + success: (sheet) => { + const sourceType = sheet.tapIndex === 0 ? ['camera'] : ['album'] + uni.chooseImage({ + count, + sizeType: ['original', 'compressed'], + sourceType, + success: (res) => resolve(res), + fail: (err) => { + if (err && err.errMsg && err.errMsg.indexOf('cancel') !== -1) { + reject(new Error('cancel')) + return + } + reject(err) + }, + }) }, fail: (err) => { if (err && err.errMsg && err.errMsg.indexOf('cancel') !== -1) { @@ -52,6 +33,50 @@ export function pickAndUploadImage(options = {}) { }) } +async function uploadChosenImages(res, count = 1) { + uni.showLoading({ title: '上传中...', mask: true }) + try { + const files = res.tempFiles || [] + const paths = files.length ? files.map((f) => f.path) : (res.tempFilePaths || []) + const urls = [] + for (const path of paths) { + const prepared = await prepareImagePath({ + path, + size: files.find((f) => f.path === path)?.size, + }) + if (!prepared) continue + const fileSize = files.find((f) => f.path === path)?.size || 0 + const result = await uploadToOss({ filePath: prepared.path, fileSize }) + urls.push(result.url) + } + uni.hideLoading() + if (!urls.length) throw new Error('上传失败') + return count === 1 ? urls[0] : urls + } catch (e) { + uni.hideLoading() + uni.showToast({ title: e.message || '上传失败', icon: 'none' }) + throw e + } +} + +/** + * 打开图库选择(由页面内 link 单独触发) + */ +export function openGalleryPick() { + return requestGalleryPick() +} + +/** + * 选择并上传图片(压缩 + OSS 直传) + * @param {{ count?: number }} options count>1 时依次上传多张,返回 url 数组 + */ +export function pickAndUploadImage(options = {}) { + const count = Math.min(Math.max(options.count || 1, 1), 9) + return chooseImageFromDevice(count) + .then((res) => uploadChosenImages(res, count)) + .catch((err) => Promise.reject(err)) +} + function getUserDataPath() { if (typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) { return wx.env.USER_DATA_PATH @@ -177,6 +202,7 @@ export function createDefaultDoctorInputForm() { card_up: '', card_down: '', sign_image: '', + register_price: 0, store_id: 0, store_ids: [], } diff --git a/subPackages/sub_salesperson/components/ImageGalleryPicker.vue b/subPackages/sub_salesperson/components/ImageGalleryPicker.vue new file mode 100644 index 0000000..2174697 --- /dev/null +++ b/subPackages/sub_salesperson/components/ImageGalleryPicker.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/subPackages/sub_salesperson/components/business-page-layout.vue b/subPackages/sub_salesperson/components/business-page-layout.vue index 3570eac..30b0a0c 100644 --- a/subPackages/sub_salesperson/components/business-page-layout.vue +++ b/subPackages/sub_salesperson/components/business-page-layout.vue @@ -10,6 +10,7 @@ + import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js'; +import ImageGalleryPicker from '@/subPackages/sub_salesperson/components/ImageGalleryPicker.vue'; export default { name: 'BusinessPageLayout', mixins: [businessMixin], + components: { ImageGalleryPicker }, props: { title: { type: String, default: '' }, showBack: { type: Boolean, default: false }, diff --git a/subPackages/sub_salesperson/doctor-input/form.vue b/subPackages/sub_salesperson/doctor-input/form.vue index 594d927..bea9984 100644 --- a/subPackages/sub_salesperson/doctor-input/form.vue +++ b/subPackages/sub_salesperson/doctor-input/form.vue @@ -28,6 +28,7 @@ 上传头像 + 从图库选择 @@ -96,6 +97,7 @@ 上传签名图 + 从图库选择 @@ -104,10 +106,14 @@ + + + 证照资料 + *{{ item.label }} @@ -117,6 +123,8 @@ 上传 + 从图库选择 + @@ -128,6 +136,7 @@ 关联门店{{ selectedStoreNames || '-' }} 科室/职称{{ selectedDepartLabel }} / {{ selectedTitleLabel }} 身份{{ selectedTypeLabel }} + 挂号费{{ form.register_price != null && form.register_price !== '' ? form.register_price : '0' }} 签名{{ form.sign_image ? '已填写' : '-' }} @@ -164,6 +173,7 @@ import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js' import { pickAndUploadImage, + openGalleryPick, uploadSignCanvasBase64, uploadSignCanvasTempPath, createDefaultDoctorInputForm, @@ -347,6 +357,9 @@ export default { uploadField(field) { pickAndUploadImage().then((url) => { this.form[field] = url }).catch(() => {}) }, + pickFromGallery(field) { + openGalleryPick().then((url) => { this.form[field] = url }).catch(() => {}) + }, onSignCanvasSave(payload) { this.showSignPad = false const base64 = typeof payload === 'string' ? payload : (payload && payload.base64) @@ -473,6 +486,7 @@ export default { if (!this.validate()) return const payload = { ...this.form, + register_price: Number(this.form.register_price) || 0, store_input_ids: this.selectedStoreInputIds, store_ids: this.selectedStoreIds, store_id: this.selectedStoreInputIds[0] || this.selectedStoreIds[0] || 0, @@ -544,6 +558,17 @@ export default { gap: 12rpx; } .sign-tip { font-size: 26rpx; color: #6acdbb; } +.upload-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24rpx; + padding-bottom: 20rpx; +} +.upload-grid .upload-group { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} .img-row, .img-list { display: flex; flex-wrap: wrap; gap: 24rpx; } .thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; } .upload-group { margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; } @@ -561,6 +586,12 @@ export default { border: 2rpx dashed #dcdfe6; } .upload-txt { font-size: 24rpx; color: #999; margin-top: 12rpx; } +.gallery-link { + display: inline-block; + margin-top: 12rpx; + font-size: 24rpx; + color: #6acdbb; +} .summary-card { background: #f7f8fa; border-radius: 12rpx; padding: 24rpx; margin-bottom: 40rpx; } .summary-row { display: flex; padding: 12rpx 0; font-size: 28rpx; } .summary-row .k { width: 180rpx; color: #86909c; flex-shrink: 0; } diff --git a/subPackages/sub_salesperson/store-input/form.vue b/subPackages/sub_salesperson/store-input/form.vue index a046699..49aef64 100644 --- a/subPackages/sub_salesperson/store-input/form.vue +++ b/subPackages/sub_salesperson/store-input/form.vue @@ -103,6 +103,7 @@ 上传公章 + 从图库选择 @@ -113,6 +114,7 @@ + 从图库选择 @@ -196,7 +198,7 @@ import RegionAddressPicker from '@/subPackages/sub_salesperson/components/Region import PromoterPicker from '@/subPackages/sub_salesperson/components/PromoterPicker.vue' import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js' import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js' -import { pickAndUploadImage, createDefaultStoreInputForm } from '@/subPackages/sub_salesperson/common/inputFormHelpers.js' +import { pickAndUploadImage, openGalleryPick, createDefaultStoreInputForm } from '@/subPackages/sub_salesperson/common/inputFormHelpers.js' import { getDraft, saveDraft, removeDraft } from '@/subPackages/sub_salesperson/common/inputDraftStorage.js' import { serializeStoreDraft, applyStoreDraft } from '@/subPackages/sub_salesperson/common/inputDraftHelpers.js' import { goDoctorInputFromStore } from '@/subPackages/sub_salesperson/common/inputNavigation.js' @@ -360,6 +362,12 @@ export default { uploadField(field) { pickAndUploadImage().then(url => { this.form[field] = url }).catch(() => {}) }, + pickFromGallery(field) { + openGalleryPick().then(url => { this.form[field] = url }).catch(() => {}) + }, + pickCarouselFromGallery() { + openGalleryPick().then(url => { this.form.url.push(url) }).catch(() => {}) + }, uploadCarousel() { pickAndUploadImage().then(url => { this.form.url.push(url) }).catch(() => {}) }, @@ -516,21 +524,21 @@ export default { .time-box { flex: 1; text-align: center; background: #f5f7fa; padding: 12rpx 0; border-radius: 8rpx; font-size: 28rpx; color: #606266; } .time-sep { margin: 0 20rpx; color: #999; } -.clinic-type-cards { display: flex; gap: 20rpx; width: 100%; } +.clinic-type-cards { display: flex; gap: 12rpx; width: 100%; } .clinic-type-card { flex: 1; - padding: 24rpx 16rpx; + padding: 12rpx 10rpx; text-align: center; background: #f5f7fa; border: 2rpx solid #e4e7ed; - border-radius: 12rpx; + border-radius: 8rpx; box-sizing: border-box; } .clinic-type-card.active { background: rgba(106, 205, 187, 0.08); border-color: #6acdbb; } -.clinic-type-label { font-size: 28rpx; color: #303133; } +.clinic-type-label { font-size: 24rpx; color: #303133; } .clinic-type-card.active .clinic-type-label { color: #6acdbb; font-weight: 600; } .switch-row { display: flex; @@ -550,6 +558,12 @@ export default { .thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); } .upload-btn { width: 160rpx; height: 160rpx; background: #f4f5f7; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 2rpx dashed #dcdfe6; } .upload-txt { font-size: 24rpx; color: #999; margin-top: 12rpx; } +.gallery-link { + display: inline-block; + margin-top: 12rpx; + font-size: 24rpx; + color: #6acdbb; +} .contract-btn { width: 100%; height: 100rpx; flex-direction: row; gap: 12rpx; background: rgba(106, 205, 187, 0.05); border-color: #6acdbb; } /* 底部操作区 */