liujie
2026-09-09 39f6fca28e3f78abe48f08e474ec011abb990b36
frontend/src/views/GuideTeamDetailView.vue
@@ -3,25 +3,38 @@
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Edit, Plus } from '@element-plus/icons-vue'
import { createGuideTeam, getGuideTeamDetail, updateGuideTeam } from '@/api/guideTeams'
import {
  createGuideTeam,
  getGuideTeamCollectionGuideOptions,
  getGuideTeamDetail,
  updateGuideTeam,
} from '@/api/guideTeams'
import { getScenicSpotDetail, getScenicSpots } from '@/api/scenicSpots'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const collectionGuideLoading = ref(false)
const collectionGuideOptions = ref([])
const formRef = ref()
const scenicFormRef = ref()
const scenicDialogVisible = ref(false)
const scenicEditingIndex = ref(-1)
const scenicOptionsLoading = ref(false)
const scenicDetailLoading = ref(false)
const scenicOptions = ref([])
const meetingPointOptions = ref([])
const isCreate = computed(() => route.name === 'guide-team-create')
const isEdit = computed(() => route.name === 'guide-team-edit')
const readonly = computed(() => route.name === 'guide-team-detail')
const pageTitle = computed(() => isCreate.value ? '新增讲解队' : isEdit.value ? '编辑讲解队' : '讲解队详情')
const form = reactive({
  id: null, teamName: '', contactPhone: '', createdAt: '', companyName: '',
  unifiedSocialCreditCode: '', commissionRatio: null, wechatOpenId: '', merchantId: '', scenicConfigs: [],
  unifiedSocialCreditCode: '', commissionRatio: null, wechatOpenId: '', merchantId: '',
  balance: 0, collectionGuideId: null, scenicConfigs: [],
})
const scenicForm = reactive({ scenicName: '', cityName: '', meetingPoints: '' })
const scenicForm = reactive({ scenicSpotId: null, scenicName: '', cityName: '', meetingPointNames: [] })
const rules = {
  teamName: [
    { required: true, message: '请输入讲解队名称', trigger: 'blur' },
@@ -36,12 +49,12 @@
  ],
}
const scenicRules = {
  scenicName: [{ required: true, message: '请输入景区名称', trigger: 'blur' }],
  cityName: [{ required: true, message: '请输入城市', trigger: 'blur' }],
  meetingPoints: [{ required: true, message: '请输入汇合点', trigger: 'blur' }],
  scenicSpotId: [{ required: true, message: '请选择景区', trigger: 'change' }],
  meetingPointNames: [{ required: true, type: 'array', min: 1, message: '请选择汇合点', trigger: 'change' }],
}
const formatDateTime = (value) => value ? value.replace('T', ' ').slice(0, 19) : '-'
const formatMoney = (value) => `¥${Number(value || 0).toFixed(2)}`
const assignDetail = (detail) => Object.assign(form, {
  id: detail.id,
  teamName: detail.teamName || '',
@@ -52,30 +65,74 @@
  commissionRatio: detail.commissionRatio ?? 0,
  wechatOpenId: detail.wechatOpenId || '',
  merchantId: detail.merchantId || '',
  balance: detail.balance ?? 0,
  collectionGuideId: detail.collectionGuideId ?? null,
  scenicConfigs: (detail.scenicConfigs || []).map((item) => ({ ...item })),
})
const loadCollectionGuideOptions = async () => {
  if (isCreate.value) return
  collectionGuideLoading.value = true
  try { collectionGuideOptions.value = await getGuideTeamCollectionGuideOptions(route.params.id) }
  finally { collectionGuideLoading.value = false }
}
const loadDetail = async () => {
  if (isCreate.value) { form.commissionRatio = 0; return }
  loading.value = true
  try { assignDetail(await getGuideTeamDetail(route.params.id)) }
  try {
    assignDetail(await getGuideTeamDetail(route.params.id))
    await loadCollectionGuideOptions()
  }
  finally { loading.value = false }
}
const loadScenicOptions = async () => {
  scenicOptionsLoading.value = true
  try {
    const result = await getScenicSpots({ page: 1, size: 100, status: 1 })
    scenicOptions.value = result.records || []
  } finally { scenicOptionsLoading.value = false }
}
const selectScenic = async (scenicSpotId, preserveMeetingPoints = false) => {
  const selected = scenicOptions.value.find((item) => item.id === scenicSpotId)
  scenicForm.scenicName = selected?.scenicName || ''
  scenicForm.cityName = selected?.cityName || ''
  if (!preserveMeetingPoints) scenicForm.meetingPointNames = []
  meetingPointOptions.value = []
  if (!scenicSpotId) return
  scenicDetailLoading.value = true
  try {
    const detail = await getScenicSpotDetail(scenicSpotId)
    meetingPointOptions.value = (detail.meetingPoints || []).filter((point) => point.status === 1)
  } finally { scenicDetailLoading.value = false }
}
const findScenicOption = (row) => scenicOptions.value.find((item) => (
  item.scenicName === row.scenicName && item.cityName === row.cityName
))
const splitMeetingPoints = (value) => String(value || '').split(/[,,]/).map((item) => item.trim()).filter(Boolean)
const openScenicCreate = () => {
  scenicEditingIndex.value = -1
  Object.assign(scenicForm, { scenicName: '', cityName: '', meetingPoints: '' })
  Object.assign(scenicForm, { scenicSpotId: null, scenicName: '', cityName: '', meetingPointNames: [] })
  meetingPointOptions.value = []
  scenicDialogVisible.value = true
}
const openScenicEdit = (row, index) => {
const openScenicEdit = async (row, index) => {
  scenicEditingIndex.value = index
  Object.assign(scenicForm, { scenicName: row.scenicName, cityName: row.cityName, meetingPoints: row.meetingPoints })
  const selected = findScenicOption(row)
  Object.assign(scenicForm, {
    scenicSpotId: selected?.id || null,
    scenicName: row.scenicName,
    cityName: row.cityName,
    meetingPointNames: splitMeetingPoints(row.meetingPoints),
  })
  meetingPointOptions.value = []
  scenicDialogVisible.value = true
  if (selected) await selectScenic(selected.id, true)
}
const saveScenic = async () => {
  await scenicFormRef.value.validate()
  const value = {
    scenicName: scenicForm.scenicName.trim(),
    cityName: scenicForm.cityName.trim(),
    meetingPoints: scenicForm.meetingPoints.trim(),
    scenicName: scenicForm.scenicName,
    cityName: scenicForm.cityName,
    meetingPoints: scenicForm.meetingPointNames.join(','),
  }
  const duplicated = form.scenicConfigs.some((item, index) => (
    index !== scenicEditingIndex.value && item.scenicName.trim() === value.scenicName
@@ -98,6 +155,10 @@
}
const save = async () => {
  await formRef.value.validate()
  if (!form.scenicConfigs.length) {
    ElMessage.warning('请至少配置一个景区')
    return
  }
  if (!form.wechatOpenId.trim() && !form.merchantId.trim()) {
    ElMessage.warning('openID和商户ID至少配置一个')
    return
@@ -112,6 +173,7 @@
      commissionRatio: form.commissionRatio,
      wechatOpenId: form.wechatOpenId.trim() || null,
      merchantId: form.merchantId.trim() || null,
      collectionGuideId: form.collectionGuideId,
      scenicConfigs: form.scenicConfigs,
    }
    const result = isCreate.value ? await createGuideTeam(payload) : await updateGuideTeam(route.params.id, payload)
@@ -121,7 +183,7 @@
  } finally { saving.value = false }
}
onMounted(loadDetail)
onMounted(() => Promise.all([loadDetail(), loadScenicOptions()]))
</script>
<template>
@@ -148,6 +210,9 @@
            </el-form-item>
            <el-form-item label="注册时间">
              <el-input :model-value="formatDateTime(form.createdAt)" disabled />
            </el-form-item>
            <el-form-item label="讲解队余额">
              <el-input :model-value="formatMoney(form.balance)" disabled />
            </el-form-item>
            <el-form-item label="企业名称">
              <el-input v-model="form.companyName" :disabled="readonly" maxlength="200" />
@@ -187,6 +252,23 @@
            <span>openID和商户ID至少配置一个</span>
          </div>
          <div class="collection-grid">
            <el-form-item label="收款导游">
              <el-select
                v-model="form.collectionGuideId"
                :disabled="readonly || isCreate"
                :loading="collectionGuideLoading"
                clearable
                placeholder="请选择收款导游"
              >
                <el-option
                  v-for="guide in collectionGuideOptions"
                  :key="guide.id"
                  :label="`${guide.realName || '-'} ${guide.mobile || ''}${guide.unavailableReason ? `(${guide.unavailableReason})` : ''}`"
                  :value="guide.id"
                  :disabled="!guide.configurable && guide.id !== form.collectionGuideId"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="openID">
              <el-input v-model="form.wechatOpenId" :disabled="readonly" maxlength="128" />
            </el-form-item>
@@ -204,14 +286,42 @@
    <el-dialog v-model="scenicDialogVisible" :title="scenicEditingIndex >= 0 ? '编辑景区配置' : '添加景区配置'" width="560px" destroy-on-close>
      <el-form ref="scenicFormRef" :model="scenicForm" :rules="scenicRules" label-width="86px">
        <el-form-item label="景区名称" prop="scenicName">
          <el-input v-model="scenicForm.scenicName" maxlength="128" />
        <el-form-item label="景区名称" prop="scenicSpotId">
          <el-select
            v-model="scenicForm.scenicSpotId"
            :loading="scenicOptionsLoading"
            filterable
            placeholder="请选择景区"
            @change="selectScenic"
          >
            <el-option
              v-for="scenic in scenicOptions"
              :key="scenic.id"
              :label="`${scenic.scenicName}(${scenic.cityName})`"
              :value="scenic.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="城市" prop="cityName">
          <el-input v-model="scenicForm.cityName" maxlength="64" />
        <el-form-item label="城市">
          <el-input :model-value="scenicForm.cityName" disabled placeholder="选择景区后自动带出" />
        </el-form-item>
        <el-form-item label="汇合点" prop="meetingPoints">
          <el-input v-model="scenicForm.meetingPoints" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="多个汇合点可使用逗号分隔" />
        <el-form-item label="汇合点" prop="meetingPointNames">
          <el-select
            v-model="scenicForm.meetingPointNames"
            :disabled="!scenicForm.scenicSpotId"
            :loading="scenicDetailLoading"
            multiple
            collapse-tags
            collapse-tags-tooltip
            placeholder="请选择汇合点"
          >
            <el-option
              v-for="point in meetingPointOptions"
              :key="point.id"
              :label="point.pointName"
              :value="point.pointName"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
@@ -250,6 +360,7 @@
.collection-grid :deep(.el-form-item__label) { width: auto !important; color: #53657d; font-weight: 600; }
.save-row { padding: 28px 36px; border-top: 10px solid #f4f7fb; }
.save-row :deep(.el-button) { width: 138px; }
.team-detail-page :deep(.el-dialog .el-select) { width: 100%; }
@media (max-width: 1400px) {
  .basic-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}