| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853 |
- <template>
- <view class="page-container">
- <!-- 页面滚动区域 -->
- <scroll-view class="page-scroll" scroll-y>
- <!-- 地块信息卡片 -->
- <view class="info-card">
- <view class="card-title">
- <text>地块信息</text>
- </view>
- <view class="info-item">
- <text class="info-label">地块名称</text>
- <text class="info-value">{{ formData.fieldName || '未知' }}</text>
- </view>
- <view class="info-item">
- <text class="info-label">作物名称</text>
- <text class="info-value">{{ formData.growCrops || '未知' }}</text>
- </view>
- <view class="info-item">
- <text class="info-label">负责人</text>
- <text class="info-value">{{ formData.manager || '未知' }}</text>
- </view>
- </view>
- <!-- 任务填写表单 -->
- <view class="form-card">
- <view class="card-title">
- <text>任务信息</text>
- </view>
-
- <!-- 任务名称 -->
- <view class="form-item">
- <view class="form-label required">任务名称</view>
- <input
- v-model="formData.taskName"
- placeholder="请输入任务名称,例如:水稻田施肥"
- :disabled="pageMode === 'view'"
- class="form-input"
- />
- </view>
- <!-- 任务类型 -->
- <view class="form-item" @click="pageMode !== 'view' && showTaskTypeSelector()">
- <view class="form-label required">任务类型</view>
- <view class="select-wrapper">
- <!-- <input
- :value="formData.typeName"
- :placeholder="dictLoading ? '加载中...' : '请选择任务类型'"
- readonly
- class="form-input select-input"
- /> -->
- <view class="form-input select-input">
- {{ formData.typeName || (dictLoading ? '加载中...' : '请选择任务类型') }}
- </view>
- <view v-if="pageMode !== 'view'" class="select-arrow">
- <text>▼</text>
- </view>
- </view>
- </view>
- <!-- 执行时间 -->
- <view class="form-item" @click="pageMode !== 'view' && selectExecuteTime()">
- <view class="form-label required">执行时间</view>
- <view class="select-wrapper">
- <!-- <input
- :value="formattedExecuteTime"
- placeholder="请选择任务计划时间"
- readonly
- class="form-input select-input"
- readonly
- /> -->
- <view class="form-input select-input">
- {{ formattedExecuteTime || '请选择任务计划时间' }}
- </view>
- <view v-if="pageMode !== 'view'" class="select-arrow">
- <text>选择</text>
- </view>
- </view>
- </view>
- <!-- 负责人选择 -->
- <view class="form-item" @click="pageMode !== 'view' && showUserSelector()">
- <view class="form-label required">负责人</view>
- <view class="select-wrapper">
- <!-- <input
- :value="formData.assigneeName"
- placeholder="请选择负责人"
- readonly
- class="form-input select-input"
- /> -->
- <view class="form-input select-input">
- {{ formData.assigneeName || '请选择负责人' }}
- </view>
- <view v-if="pageMode !== 'view'" class="select-arrow">
- <text>选择</text>
- </view>
- </view>
- </view>
- <!-- 任务说明 -->
- <view class="form-item">
- <view class="form-label">任务说明</view>
- <textarea
- v-model="formData.remark"
- placeholder="请输入任务要点,例如:每亩用肥20kg"
- :disabled="pageMode === 'view'"
- class="form-textarea"
- maxlength="200"
- ></textarea>
- <view class="char-count">{{ (formData.remark || '').length }}/200</view>
- </view>
- <!-- 任务完成情况 -->
- <view class="section-divider"></view>
- <view class="section-title">
- <text>任务完成情况</text>
- </view>
- <!-- 完成状态选择 - 新建和编辑模式 -->
- <view class="form-item" v-if="pageMode === 'create' || pageMode === 'edit'">
- <view class="form-label">完成状态</view>
- <view class="radio-group">
- <view
- class="radio-item"
- v-for="(item, index) in completionStatusOptions"
- :key="index"
- @click="selectCompletionStatus(item.value)"
- >
- <view class="radio-circle" :class="{'radio-checked': formData.completionStatus === item.value}">
- <view v-if="formData.completionStatus === item.value" class="radio-dot"></view>
- </view>
- <text class="radio-label">{{ item.label }}</text>
- </view>
- </view>
- </view>
- <!-- 查看模式显示完成状态 -->
- <view class="form-item" v-if="pageMode === 'view'">
- <view class="form-label">完成状态</view>
- <view class="status-completed">
- <text class="status-icon">✓</text>
- <text>已完成</text>
- </view>
- </view>
- <!-- 完成时间 -->
- <view class="form-item" v-if="formData.completionStatus === '1'">
- <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成时间</view>
- <view class="select-wrapper" @click="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1' && selectCompletionTime()">
- <!-- <input
- :value="formattedCompletionTime"
- placeholder="请选择实际完成时间"
- readonly
- class="form-input select-input"
- /> -->
- <view class="form-input select-input">
- {{ formattedCompletionTime || '请选择实际完成时间' }}
- </view>
- <view v-if="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'" class="select-arrow">
- <text>选择</text>
- </view>
- </view>
- </view>
- <!-- 完成说明 -->
- <view class="form-item" v-if="formData.completionStatus === '1'">
- <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成说明</view>
- <textarea
- v-model="formData.completionDesc"
- placeholder="请输入完成说明,例如:已完成并拍照记录"
- :disabled="pageMode === 'view'"
- class="form-textarea"
- maxlength="300"
- ></textarea>
- <view class="char-count">{{ (formData.completionDesc || '').length }}/300</view>
- <view class="form-error" v-if="formErrors.completionDesc">
- {{ formErrors.completionDesc }}
- </view>
- </view>
- <!-- 现场图片 -->
- <view class="form-item" v-if="formData.completionStatus === '1'">
- <view class="form-label">现场图片</view>
-
- <!-- 新建和编辑模式 -->
- <view v-if="pageMode === 'create' || pageMode === 'edit'" class="image-upload">
- <view class="image-list">
- <view
- class="image-preview"
- v-for="(item, index) in formData.images"
- :key="index"
- @click="previewImage(item, index)"
- >
- <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
- <image :src="item.url" mode="aspectFill"/>
- <view class="delete-btn" @click.stop="deletePic(index)">
- <text>×</text>
- </view>
- </view>
- <view
- v-if="formData.images.length < 6"
- class="upload-btn"
- @click="chooseImage"
- >
- <text class="upload-icon">+</text>
- <text class="upload-text">添加图片</text>
- </view>
- </view>
- <view class="upload-tip">最多可上传6张图片</view>
- </view>
- <!-- 查看模式 -->
- <view v-else-if="pageMode === 'view' && formData.images && formData.images.length > 0" class="image-view">
- <view class="image-list">
- <view
- class="image-preview"
- v-for="(item, index) in formData.images"
- :key="index"
- @click="previewImage(item, index)"
- >
- <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
- <image :src="item.url" mode="aspectFill"/>
- </view>
- </view>
- </view>
- <!-- 无图片提示 -->
- <view v-else class="no-images">
- <text>暂无图片</text>
- </view>
- </view>
- </view>
- <!-- 底部占位 -->
- <view class="bottom-safe"></view>
- </scroll-view>
- <!-- 底部提交按钮 -->
- <view class="footer-safe" v-if="pageMode !== 'view'">
- <view class="footer-content">
- <button
- class="submit-button"
- :class="{'loading': isSubmitting}"
- @click="submitForm"
- :disabled="isSubmitting"
- >
- {{ isSubmitting ? '提交中...' : submitButtonText }}
- </button>
- </view>
- </view>
- <!-- 遮罩层 -->
- <view v-if="showTaskTypePicker || showDateTimeSelector || showUserPicker" class="picker-mask" @click="closePickers"></view>
-
- <!-- 任务类型选择弹窗 -->
- <view v-if="showTaskTypePicker" class="picker-popup">
- <view class="picker-header">
- <text class="picker-cancel" @click="showTaskTypePicker = false">取消</text>
- <text class="picker-title">选择任务类型</text>
- <text class="picker-confirm" @click="confirmTaskType">确定</text>
- </view>
- <view class="picker-content">
- <view v-if="dictLoading" class="picker-loading">
- <text>加载中...</text>
- </view>
- <view
- v-else
- class="picker-item"
- v-for="(item, index) in taskTypeOptions"
- :key="index"
- :class="{'selected': tempTaskTypeIndex === index}"
- @click="tempTaskTypeIndex = index"
- >
- <text>{{ item.dictLabel }}</text>
- <text v-if="tempTaskTypeIndex === index" class="check-mark">✓</text>
- </view>
- </view>
- </view>
- <!-- 用户选择弹窗 -->
- <view v-if="showUserPicker" class="picker-popup">
- <view class="picker-header">
- <text class="picker-cancel" @click="showUserPicker = false">取消</text>
- <text class="picker-title">选择负责人</text>
- <text class="picker-confirm" @click="confirmUser">确定</text>
- </view>
- <view class="picker-content">
- <view v-if="usersLoading" class="picker-loading">
- <text>加载中...</text>
- </view>
- <view v-else-if="userList.length === 0" class="picker-empty">
- <text>暂无可选负责人</text>
- </view>
- <view
- v-else
- class="picker-item"
- v-for="(user, index) in userList"
- :key="user.userId"
- :class="{'selected': tempUserIndex === index}"
- @click="tempUserIndex = index"
- >
- <text>{{ user.userName }}</text>
- <text v-if="tempUserIndex === index" class="check-mark">✓</text>
- </view>
- </view>
- </view>
- <!-- 日期时间选择弹窗 -->
- <view v-if="showDateTimeSelector" class="picker-popup datetime-picker">
- <view class="picker-header">
- <text class="picker-cancel" @click="cancelDateTime">取消</text>
- <text class="picker-title">选择时间</text>
- <text class="picker-confirm" @click="confirmDateTime">确定</text>
- </view>
- <view class="datetime-picker-content">
- <picker-view
- class="datetime-picker-view"
- :value="dateTimePickerValue"
- @change="onDateTimePickerChange"
- :indicator-style="'height: 80rpx;'"
- :mask-style="'background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)), linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));'"
- >
- <picker-view-column>
- <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[0]" :key="'year-'+index">
- <text class="picker-item-text">{{ item }}</text>
- </view>
- </picker-view-column>
- <picker-view-column>
- <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[1]" :key="'month-'+index">
- <text class="picker-item-text">{{ item }}</text>
- </view>
- </picker-view-column>
- <picker-view-column>
- <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[2]" :key="'day-'+index">
- <text class="picker-item-text">{{ item }}</text>
- </view>
- </picker-view-column>
- <picker-view-column>
- <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[3]" :key="'hour-'+index">
- <text class="picker-item-text">{{ item }}</text>
- </view>
- </picker-view-column>
- <picker-view-column>
- <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[4]" :key="'min-'+index">
- <text class="picker-item-text">{{ item }}</text>
- </view>
- </picker-view-column>
- </picker-view>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, reactive, computed } from 'vue'
- import api from "@/config/api.js";
- import { getAgriculturalTasksById, addAgriculturalTask, updateAgriculturalTask } from '@/api/services/activity.js';
- import { getUsersByPlotId, getUserInfo } from '@/api/services/user.js';
- import { useDict } from '@/utils/composables/useDict';
- import storage from "@/utils/storage.js";
- // 使用字典组合式函数
- const { dictData, dictLoading, loadDict } = useDict(['task_type', 'task_status'])
- // 响应式数据
- </script>
- <style scoped>
- .page-container {
- height: 100vh;
- background-color: #F5F5F5;
- display: flex;
- flex-direction: column;
- }
- .page-scroll {
- flex: 1;
- overflow: hidden;
- }
- /* 地块基础信息卡片 */
- .info-card {
- background: #F9F9F9;
- margin: 24rpx;
- padding: 24rpx;
- border-radius: 8rpx;
- border: 1rpx solid #E5E5E5;
- }
- .card-title {
- font-size: 30rpx;
- font-weight: 600;
- color: #333333;
- margin-bottom: 16rpx;
- padding-bottom: 12rpx;
- border-bottom: 1rpx solid #E5E5E5;
- }
- .info-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12rpx;
- padding: 8rpx 0;
- }
- .info-item:last-child {
- margin-bottom: 0;
- }
- .info-label {
- font-size: 28rpx;
- color: #666666;
- flex-shrink: 0;
- }
- .info-value {
- font-size: 28rpx;
- color: #333333;
- font-weight: 500;
- }
- /* 任务填写表单 */
- .form-card {
- background: #FFFFFF;
- margin: 0 24rpx 24rpx;
- padding: 24rpx;
- border-radius: 8rpx;
- border: 1rpx solid #E5E5E5;
- }
- .section-divider {
- height: 1rpx;
- background: #F0F0F0;
- margin: 32rpx 0 24rpx;
- }
- .section-title {
- font-size: 30rpx;
- font-weight: 600;
- color: #333333;
- margin-bottom: 16rpx;
- padding-bottom: 12rpx;
- border-bottom: 1rpx solid #F0F0F0;
- }
- .form-item {
- margin-bottom: 24rpx;
- }
- .form-item:last-child {
- margin-bottom: 0;
- }
- .form-label {
- font-size: 28rpx;
- color: #333333;
- margin-bottom: 12rpx;
- font-weight: 500;
- }
- .form-label.required::before {
- content: '*';
- color: #FF6B6B;
- margin-right: 4rpx;
- }
- .form-input {
- width: 100%;
- line-height: 80rpx;
- height: 80rpx;
- background: #F8F8F8;
- border: 1rpx solid #E5E5E5;
- border-radius: 8rpx;
- padding: 0 16rpx;
- font-size: 28rpx;
- color: #333333;
- box-sizing: border-box;
- }
- .form-input:focus {
- background: #FFFFFF;
- border-color: #3BB44A;
- }
- .form-input::placeholder {
- color: #CCCCCC;
- }
- .form-input:disabled {
- background: #F8F8F8;
- color: #999999;
- }
- .select-wrapper {
- position: relative;
- width: 100%;
- }
- .select-input {
- cursor: pointer;
- }
- .select-arrow {
- position: absolute;
- right: 16rpx;
- top: 50%;
- transform: translateY(-50%);
- color: #999999;
- font-size: 20rpx;
- pointer-events: none;
- }
- .form-textarea {
- width: 100%;
- min-height: 120rpx;
- background: #F8F8F8;
- border: 1rpx solid #E5E5E5;
- border-radius: 8rpx;
- padding: 16rpx;
- font-size: 28rpx;
- color: #333333;
- box-sizing: border-box;
- resize: none;
- line-height: 1.5;
- }
- .form-textarea:focus {
- background: #FFFFFF;
- border-color: #3BB44A;
- }
- .form-textarea::placeholder {
- color: #CCCCCC;
- }
- .form-textarea:disabled {
- background: #F8F8F8;
- color: #999999;
- }
- .char-count {
- font-size: 24rpx;
- color: #CCCCCC;
- text-align: right;
- margin-top: 8rpx;
- }
- .form-error {
- font-size: 24rpx;
- color: #FF6B6B;
- margin-top: 8rpx;
- }
- /* 单选按钮样式 */
- .radio-group {
- display: flex;
- gap: 32rpx;
- }
- .radio-item {
- display: flex;
- align-items: center;
- cursor: pointer;
- }
- .radio-circle {
- width: 32rpx;
- height: 32rpx;
- border: 2rpx solid #CCCCCC;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 8rpx;
- }
- .radio-circle.radio-checked {
- border-color: #3BB44A;
- background-color: #3BB44A;
- }
- .radio-dot {
- width: 12rpx;
- height: 12rpx;
- background-color: #FFFFFF;
- border-radius: 50%;
- }
- .radio-label {
- font-size: 28rpx;
- color: #333333;
- }
- /* 完成状态样式 */
- .status-completed {
- display: flex;
- align-items: center;
- color: #3BB44A;
- font-size: 28rpx;
- }
- .status-icon {
- width: 32rpx;
- height: 32rpx;
- background: #3BB44A;
- color: #FFFFFF;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 8rpx;
- font-size: 20rpx;
- line-height: 1;
- }
- /* 图片上传样式 */
- .image-upload, .image-view {
- width: 100%;
- }
- .image-list {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- }
- .image-preview {
- position: relative;
- width: 160rpx;
- height: 160rpx;
- border-radius: 8rpx;
- overflow: hidden;
- background: #F5F5F5;
- }
- .image-preview image {
- width: 100%;
- height: 100%;
- }
- .delete-btn {
- position: absolute;
- top: -6rpx;
- right: -6rpx;
- width: 32rpx;
- height: 32rpx;
- background: rgba(0, 0, 0, 0.6);
- color: #FFFFFF;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 20rpx;
- line-height: 1;
- }
- .upload-btn {
- width: 160rpx;
- height: 160rpx;
- background: #F8F8F8;
- border: 2rpx dashed #DDDDDD;
- border-radius: 8rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }
- .upload-icon {
- font-size: 32rpx;
- margin-bottom: 8rpx;
- color: #999999;
- }
- .upload-text {
- font-size: 24rpx;
- color: #999999;
- }
- .upload-tip {
- font-size: 24rpx;
- color: #999999;
- margin-top: 12rpx;
- }
- .no-images {
- padding: 40rpx 0;
- text-align: center;
- color: #CCCCCC;
- font-size: 28rpx;
- }
- /* 底部安全区域 */
- .bottom-safe {
- height: 120rpx;
- }
- .footer-safe {
- background: #FFFFFF;
- border-top: 1rpx solid #E5E5E5;
- padding-bottom: constant(safe-area-inset-bottom);
- padding-bottom: env(safe-area-inset-bottom);
- }
- .footer-content {
- padding: 24rpx;
- }
- .submit-button {
- width: 100%;
- height: 88rpx;
- background: #3BB44A;
- color: #FFFFFF;
- border: none;
- border-radius: 8rpx;
- font-size: 32rpx;
- font-weight: 600;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .submit-button:active {
- background: #2D8C3C;
- }
- .submit-button:disabled,
- .submit-button.loading {
- background: #CCCCCC;
- }
- /* 选择器弹窗样式 */
- .picker-mask {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- z-index: 999;
- }
- .picker-popup {
- position: fixed;
- bottom: 0;
- left: 0;
- width: 100%;
- background: #FFFFFF;
- border-radius: 16rpx 16rpx 0 0;
- z-index: 1000;
- padding-bottom: constant(safe-area-inset-bottom);
- padding-bottom: env(safe-area-inset-bottom);
- }
- .picker-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24rpx;
- border-bottom: 1rpx solid #E5E5E5;
- }
- .picker-cancel, .picker-confirm {
- font-size: 30rpx;
- color: #3BB44A;
- }
- .picker-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #333333;
- }
- .picker-content {
- max-height: 400rpx;
- overflow-y: auto;
- }
- .picker-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24rpx;
- border-bottom: 1rpx solid #F0F0F0;
- font-size: 30rpx;
- color: #333333;
- }
- .picker-item.selected {
- color: #3BB44A;
- }
- .picker-item:last-child {
- border-bottom: none;
- }
- .check-mark {
- color: #3BB44A;
- font-size: 28rpx;
- }
- /* 日期时间选择器样式 */
- .datetime-picker {
- height: 60vh;
- }
- .datetime-picker-content {
- height: calc(100% - 100rpx);
- padding: 0;
- }
- .datetime-picker-view {
- width: 100%;
- height: 100%;
- }
- .picker-view-item {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 80rpx;
- font-size: 32rpx;
- color: #333333;
- }
- .picker-item-text {
- font-size: 32rpx;
- color: #333333;
- line-height: 80rpx;
- height: 80rpx;
- text-align: center;
- }
- /* 添加加载中样式 */
- .picker-loading {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 30rpx 0;
- color: #999;
- font-size: 28rpx;
- }
- .picker-empty {
- padding: 30rpx 0;
- text-align: center;
- color: #CCCCCC;
- font-size: 28rpx;
- }
- </style>
|