| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053 |
- <template>
- <div class="page-container">
- <el-card shadow="never">
- <template #header>
- <div class="card-header">
- <span class="title">用户管理</span>
- <div class="header-actions">
- <el-cascader v-model="searchRegionValue" :options="areaCascaderOptions"
- :props="{ value: 'id', label: 'name' }"
- placeholder="所属站点" style="width: 350px; margin-right: 10px" clearable @change="handleSearchRegionChange" />
- <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px;" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
- <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['archieves:customer:add']">新增用户</el-button>
- </div>
- </div>
- </template>
- <el-table :data="tableData" v-loading="loading" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
- <el-table-column label="用户基本信息" width="250">
- <template #default="scope">
- <div style="display: flex; align-items: center;">
- <el-avatar :size="40" :src="scope.row.avatarUrl" style="margin-right: 10px;" />
- <div>
- <div style="font-weight: bold;">{{ scope.row.name }}
- <dict-tag :options="sys_user_sex" :value="scope.row.gender" />
- </div>
- <div style="font-size: 12px; color: #999;">{{ scope.row.phone }}</div>
- </div>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="住址" show-overflow-tooltip min-width="150">
- <template #default="scope">
- {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToText[c] || '').filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="用户标签" width="200">
- <template #default="scope">
- <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px;">{{ tag.name }}</el-tag>
- </template>
- </el-table-column>
- <!-- <el-table-column label="录入信息" width="200">-->
- <!-- <template #default="scope">-->
- <!-- <div v-if="scope.row.tenantName" style="margin-bottom: 4px;">-->
- <!-- <el-tag size="small" effect="light">{{ scope.row.tenantName }}</el-tag>-->
- <!-- </div>-->
- <!--<!– <div><el-tag size="small" effect="plain" :type="scope.row.source && scope.row.source.includes('平台') ? '' : 'warning'">{{ scope.row.source || '-' }}</el-tag></div>–>-->
- <!-- <div style="font-size: 12px; color: #999; margin-top: 4px;">{{ scope.row.createTime }}</div>-->
- <!-- </template>-->
- <!-- </el-table-column>-->
- <el-table-column label="订单数量" width="120" align="center" sortable prop="orderCount">
- <template #default="scope">
- <div>{{ scope.row.orderCount }}单</div>
- </template>
- </el-table-column>
- <el-table-column prop="petCount" label="关联宠物" width="100" align="center">
- <template #default="scope">
- <el-tag size="small" round>{{ scope.row.petCount }}只</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="状态" width="100" align="center">
- <template #default="scope">
- <el-switch
- v-model="scope.row.status"
- :active-value="0"
- :inactive-value="1"
- inline-prompt
- active-text="正常"
- inactive-text="停用"
- @change="handleStatusChange(scope.row)"
- />
- </template>
- </el-table-column>
- <!-- <el-table-column prop="remark" label="备注" show-overflow-tooltip />-->
- <el-table-column label="操作" width="200" align="center">
- <template #default="scope">
- <el-button link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['archieves:customer:query']">详情</el-button>
- <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['archieves:customer:edit']">编辑</el-button>
- <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)" style="margin-left: 10px; vertical-align: middle">
- <el-button link type="primary" size="small">
- 更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="remark" v-hasPermi="['archieves:customer:remark']">添加备注</el-dropdown-item>
- <el-dropdown-item command="delete" style="color: #F56C6C" v-hasPermi="['archieves:customer:remove']">删除用户</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="queryParams.pageNum"
- v-model:page-size="queryParams.pageSize"
- :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="getList"
- @current-change="getList"
- />
- </div>
- </el-card>
- <!-- User Detail Drawer -->
- <CustomerDetailDrawer
- ref="customerDetailRef"
- v-model:visible="drawerVisible"
- :customer-id="currentCustomerId"
- editable
- @add-pet="openAddPet"
- @pet-detail="handlePetDetail"
- @pet-edit="handlePetEdit"
- @pet-remark="handlePetRemark"
- @pet-delete="handlePetDelete"
- />
- <!-- Pet Profile Drawer -->
- <pet-detail-drawer v-model:visible="petDrawerVisible" :pet-id="selectedPetId" editable @remark-saved="getList" />
- <!-- Add/Edit User Dialog -->
- <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="700px" destroy-on-close>
- <el-form :model="form" label-width="90px" class="user-form">
- <el-row :gutter="20">
- <el-col :span="24" style="text-align: center; margin-bottom: 25px;">
- <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserUploadFile">
- <el-avatar :size="80" :src="userAvatarDisplayUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="upload-avatar" />
- <div style="margin-top: 8px; font-size: 12px; color: #409EFF;">点击修改头像</div>
- </el-upload>
- </el-col>
- <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
- <!-- <el-col :span="12">-->
- <!-- <el-form-item label="录入来源">-->
- <!-- <PageSelect v-model="form.tenantId"-->
- <!-- :options="brandList.map(item => ({ value: item.id, label: item.name }))"-->
- <!-- :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"-->
- <!-- @page-change="handleBrandPageChange"-->
- <!-- @visible-change="handleBrandVisibleChange" />-->
- <!-- </el-form-item>-->
- <!-- </el-col>-->
- <el-col :span="24">
- <el-form-item label="所属站点" required>
- <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择站点"
- :props="{ value: 'value', label: 'label' }"
- style="width: 100%" clearable @change="handleFormAreaChange" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="电话" required><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="性别">
- <el-select v-model="form.gender" placeholder="请选择">
- <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
- <el-col :span="24">
- <el-form-item label="所在地区">
- <el-cascader
- v-model="regionCascaderValue"
- :options="regionData"
- placeholder="请选择省/市/区"
- style="width: 100%"
- clearable
- />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="详细住址" required><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="房屋类型">
- <el-radio-group v-model="form.houseType">
- <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="入门方式" required>
- <el-radio-group v-model="form.entryMethod">
- <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12" v-if="form.entryMethod === 'password'">
- <el-form-item label="开门密码" required>
- <el-input v-model="form.entryPassword" placeholder="请输入密码" />
- </el-form-item>
- </el-col>
- <el-col :span="12" v-if="form.entryMethod === 'key'">
- <el-form-item label="钥匙位置" required>
- <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
- </el-form-item>
- </el-col>
- <el-col :span="24"><div class="form-section-header">其他</div></el-col>
- <el-col :span="24">
- <el-form-item label="用户标签">
- <el-select v-model="selectedTagIds" multiple placeholder="选择标签" style="width: 100%">
- <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
- <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
- </el-option>
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="备注说明"><el-input type="textarea" v-model="form.remark" rows="3" /></el-form-item>
- </el-col>
- </el-row>
- </el-form>
- <div style="text-align: center; margin-top: 20px;">
- <el-button @click="dialogVisible = false" size="large" style="width: 120px;">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="saveUser" size="large" style="width: 120px;">保存</el-button>
- </div>
- </el-dialog>
- <!-- Remark Dialog -->
- <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
- <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入备注内容..." />
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="remarkDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="saveRemark">保存</el-button>
- </span>
- </template>
- </el-dialog>
- <!-- Full Add/Edit Pet Dialog -->
- <el-dialog v-model="petDialogVisible" :title="petForm.id ? '编辑宠物' : '新增宠物'" width="800px">
- <el-tabs v-model="petDialogActiveTab">
- <el-tab-pane label="基本信息" name="basic">
- <el-form :model="petForm" label-width="100px">
- <el-row>
- <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px;">
- <el-upload
- class="avatar-uploader"
- action="#"
- :show-file-list="false"
- :auto-upload="false"
- :on-change="handlePetUploadFile"
- >
- <el-avatar v-if="petAvatarDisplayUrl" :src="petAvatarDisplayUrl" :size="80" />
- <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
- </el-upload>
- </el-col>
- <el-col :span="12">
- <el-form-item label="宠物姓名" required><el-input v-model="petForm.name" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="性别">
- <el-select v-model="petForm.gender" placeholder="请选择">
- <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="品种" required>
- <el-select v-model="petForm.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
- <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="体型" required>
- <el-select v-model="petForm.size" style="width: 100%">
- <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="体重(kg)" required><el-input-number v-model="petForm.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="年龄(岁)" required><el-input-number v-model="petForm.age" :min="0" style="width: 100%" /></el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="性格关键词"><el-input v-model="petForm.personality" placeholder="如:活泼、粘人" /></el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="萌宠性格"><el-input v-model="petForm.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="宠物标签">
- <el-select v-model="petForm.tagIds" multiple placeholder="选择标签" style="width: 100%">
- <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
- <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
- </el-option>
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-tab-pane>
- <el-tab-pane label="家庭信息" name="family">
- <el-form :model="petForm" label-width="120px">
- <el-form-item label="新来家庭时间">
- <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
- </el-form-item>
- <el-form-item label="家庭房屋类型" required>
- <el-radio-group v-model="petForm.houseType">
- <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="入门方式" required>
- <el-radio-group v-model="petForm.entryMethod">
- <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="密码" v-if="petForm.entryMethod === 'password'" required>
- <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
- </el-form-item>
- <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'" required>
- <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
- </el-form-item>
- </el-form>
- </el-tab-pane>
- <el-tab-pane label="健康状况" name="health">
- <el-form :model="petForm" label-width="120px">
- <el-form-item label="健康状态" required>
- <el-radio-group v-model="petForm.healthStatus">
- <el-radio label="健康">健康</el-radio>
- <el-radio label="亚健康">亚健康</el-radio>
- <el-radio label="疾病">疾病</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="是否有攻击倾向" required>
- <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
- </el-form-item>
- <el-form-item label="疫苗情况" required>
- <el-radio-group v-model="petForm.vaccineStatus">
- <el-radio label="无">无</el-radio>
- <el-radio label="已打1次">已打1次</el-radio>
- <el-radio label="已打2次">已打2次</el-radio>
- <el-radio label="已打3次">已打3次</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="疫苗凭证">
- <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handlePetUploadVaccineCert">
- <img v-if="petVaccineCertDisplayUrl" :src="petVaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover;" />
- <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px;"><Plus /></el-icon>
- </el-upload>
- </el-form-item>
- <el-form-item label="既往病史" required>
- <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
- </el-form-item>
- <el-form-item label="过敏史" required>
- <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
- </el-form-item>
- </el-form>
- </el-tab-pane>
- </el-tabs>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="petDialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="savePet">保存</el-button>
- </span>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted, getCurrentInstance, toRefs } from 'vue'
- import { globalHeaders } from '@/utils/request'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, changeCustomerStatus, updateCustomerRemark } from '@/api/archieves/customer'
- import { listAllTag } from '@/api/archieves/tag'
- import { listPetByUser, addPet, updatePet, delPet, updatePetRemark } from '@/api/archieves/pet'
- import { listAllChangeLog } from '@/api/archieves/changeLog'
- import { listAreaStation } from '@/api/system/areaStation'
- import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
- import { regionData, codeToText } from 'element-china-area-data'
- import PageSelect from '@/components/PageSelect/index.vue'
- import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
- import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
- import { useUserStore } from '@/store/modules/user'
- const userStore = useUserStore()
- const { proxy } = getCurrentInstance()
- const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
- proxy?.useDict('sys_user_sex', 'sys_customer_status', 'sys_house_type', 'sys_entry_method', 'sys_pet_gender', 'sys_pet_size', 'sys_pet_type', 'sys_pet_breed')
- )
- const loading = ref(false)
- const submitLoading = ref(false)
- const total = ref(0)
- const allNodes = ref([])
- const searchRegionValue = ref([])
- const areaCascaderOptions = computed(() => {
- const buildTree = (data, parentId) => {
- return data
- .filter(item => String(item.parentId) === String(parentId))
- .map(item => {
- const children = buildTree(data, item.id)
- const node = { id: item.id, name: item.name }
- if (children.length > 0) {
- node.children = children
- } else if (String(item.type) !== '2') {
- node.disabled = true
- }
- return node
- })
- }
- return buildTree(allNodes.value, 0)
- })
- const loadAreaStation = () => {
- listAreaStation().then((res) => {
- allNodes.value = res.data || []
- })
- }
- const handleSearchRegionChange = (value) => {
- if (value && value.length > 0) {
- const lastId = value[value.length - 1]
- const node = allNodes.value.find(n => String(n.id) === String(lastId))
- if (node && String(node.type) === '2') {
- searchForm.stationId = lastId
- searchForm.areaId = node.parentId
- } else {
- searchForm.areaId = lastId
- searchForm.stationId = undefined
- }
- } else {
- searchForm.areaId = undefined
- searchForm.stationId = undefined
- }
- handleSearch()
- }
- const queryParams = reactive({
- pageNum: 1,
- pageSize: 10,
- keyword: '',
- areaId: undefined,
- stationId: undefined,
- status: undefined
- })
- const searchForm = reactive({
- keyword: '',
- areaId: undefined,
- stationId: undefined
- })
- const dialogVisible = ref(false)
- const drawerVisible = ref(false)
- const remarkDialogVisible = ref(false)
- const petDialogVisible = ref(false)
- const isEdit = ref(false)
- const detailActiveTab = ref('info')
- const petDialogActiveTab = ref('basic')
- const selectedTagIds = ref([])
- const currentUser = ref({})
- const tableData = ref([])
- const allUserTags = ref([])
- const allPetTags = ref([])
- const currentCustomerId = ref(null)
- const customerDetailRef = ref(null)
- const userAvatarDisplayUrl = ref('')
- const petAvatarDisplayUrl = ref('')
- const petVaccineCertDisplayUrl = ref('')
- const petDrawerVisible = ref(false)
- const selectedPetId = ref(null)
- const brandList = ref([])
- const brandTotal = ref(0)
- const formAreaValue = ref([])
- const regionCascaderValue = ref([])
- // 移除 mockOrders
- const form = reactive({
- id: undefined,
- name: '',
- phone: '',
- avatar: undefined,
- gender: undefined,
- birthday: '',
- idCard: '',
- areaId: undefined,
- stationId: undefined,
- regionCode: '',
- region: [],
- address: '',
- houseType: '',
- entryMethod: '',
- entryPassword: '',
- keyLocation: '',
- tenantId: undefined,
- emergencyContact: '',
- emergencyPhone: '',
- memberLevel: 0,
- status: 0,
- remark: '',
- tagIds: []
- })
- const petForm = reactive({
- id: undefined,
- userId: undefined,
- avatar: undefined,
- name: '',
- type: 0,
- gender: undefined,
- breed: '',
- birthday: '',
- age: 1,
- weight: 5,
- size: 'small',
- isSterilized: 0,
- arrivalTime: '',
- houseType: '',
- entryMethod: '',
- entryPassword: '',
- keyLocation: '',
- personality: '',
- cutePersonality: '',
- healthStatus: '',
- aggression: 0,
- vaccineStatus: '',
- vaccineCert: undefined,
- medicalHistory: '',
- allergies: '',
- remark: '',
- tagIds: []
- })
- const remarkForm = reactive({ content: '' })
- const areaTreeOptions = computed(() => {
- const buildTree = (data, parentId) => {
- return data
- .filter(item => String(item.parentId) === String(parentId))
- .map(item => {
- const children = buildTree(data, item.id)
- const node = { value: item.id, label: item.name }
- if (children.length > 0) {
- node.children = children
- } else if (String(item.type) !== '2') {
- node.disabled = true
- }
- return node
- })
- }
- const areaData = allNodes.value
- return buildTree(areaData, 0)
- })
- const handleFormAreaChange = (value) => {
- if (value && value.length > 0) {
- const lastId = value[value.length - 1]
- const node = allNodes.value.find(n => String(n.id) === String(lastId))
- if (node) {
- if (String(node.type) === '2') {
- form.stationId = lastId
- form.areaId = node.parentId
- } else {
- form.areaId = lastId
- form.stationId = undefined
- }
- }
- } else {
- form.areaId = undefined
- form.stationId = undefined
- }
- }
- const getBrandList = async (pageNum = 1) => {
- const res = await listBrandOnStore({ pageNum, pageSize: 10 })
- if (res.code === 200) {
- brandList.value = res.rows || []
- brandTotal.value = res.total || 0
- }
- }
- const handleBrandPageChange = (page) => {
- getBrandList(Number(page))
- }
- const handleBrandVisibleChange = (visible) => {
- if (visible) {
- getBrandList(1)
- }
- }
- const getList = () => {
- loading.value = true
- queryParams.keyword = searchForm.keyword
- queryParams.areaId = searchForm.areaId || undefined
- queryParams.stationId = searchForm.stationId || undefined
- listCustomer(queryParams).then((res) => {
- tableData.value = res.rows
- total.value = res.total
- }).finally(() => {
- loading.value = false
- })
- }
- const handleSearch = () => {
- queryParams.pageNum = 1
- getList()
- }
- const loadTags = () => {
- listAllTag({ category: 'customer', status: 0 }).then((res) => {
- allUserTags.value = res.data || []
- }).catch((err) => {
- console.error('加载用户标签失败', err)
- })
- listAllTag({ category: 'pet', status: 0 }).then((res) => {
- allPetTags.value = res.data || []
- }).catch((err) => {
- console.error('加载宠物标签失败', err)
- })
- }
- const handleAdd = () => {
- isEdit.value = false
- selectedTagIds.value = []
- Object.assign(form, {
- id: undefined, name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
- areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
- houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', tenantId: userStore.tenantId,
- emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
- })
- userAvatarDisplayUrl.value = ''
- formAreaValue.value = []
- regionCascaderValue.value = []
- dialogVisible.value = true
- }
- const handleEdit = (row) => {
- isEdit.value = true
- getCustomer(row.id).then((res) => {
- const data = res.data
- Object.assign(form, {
- id: data.id, name: data.name, phone: data.phone, avatar: data.avatar, gender: data.gender,
- birthday: data.birthday, idCard: data.idCard, areaId: data.areaId, stationId: data.stationId,
- regionCode: data.regionCode, region: data.regionCode ? data.regionCode.split('/') : [],
- address: data.address, houseType: data.houseType, entryMethod: data.entryMethod,
- entryPassword: data.entryPassword, keyLocation: data.keyLocation, tenantId: data.tenantId,
- emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
- memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
- })
- userAvatarDisplayUrl.value = data.avatarUrl || ''
- // Restore area cascader value path
- const targetId = data.stationId || data.areaId
- if (targetId) {
- const findPath = (nodes, targetId, path = []) => {
- for (const node of nodes) {
- const currentPath = [...path, node.id]
- if (String(node.id) === String(targetId)) return currentPath
- const children = allNodes.value.filter(n => String(n.parentId) === String(node.id))
- if (children.length > 0) {
- const result = findPath(children, targetId, currentPath)
- if (result) return result
- }
- }
- return null
- }
- const roots = allNodes.value.filter(n => String(n.parentId) === '0')
- formAreaValue.value = findPath(roots, targetId) || []
- } else {
- formAreaValue.value = []
- }
- // Restore region cascader value
- regionCascaderValue.value = data.regionCode ? data.regionCode.split('/') : []
- selectedTagIds.value = data.tags ? data.tags.map(t => t.id) : []
- dialogVisible.value = true
- })
- }
- const handleDetail = (row) => {
- currentCustomerId.value = row.id
- drawerVisible.value = true
- }
- // 移除不需要的方法
- const remarkTargetType = ref('customer')
- const handleRemark = (row) => {
- currentUser.value = row
- remarkTargetType.value = 'customer'
- remarkForm.content = row.remark || ''
- remarkDialogVisible.value = true
- }
- const saveRemark = () => {
- if (!remarkForm.content) return ElMessage.warning('请输入内容')
- if (remarkTargetType.value === 'customer') {
- const data = {
- id: currentUser.value.id,
- content: remarkForm.content
- }
- updateCustomerRemark(data).then(() => {
- ElMessage.success('备注添加成功')
- remarkDialogVisible.value = false
- getList()
- })
- } else {
- const data = {
- petId: currentUser.value.id,
- content: remarkForm.content
- }
- updatePetRemark(data).then(() => {
- ElMessage.success('宠物备注添加成功')
- remarkDialogVisible.value = false
- if (customerDetailRef.value) {
- customerDetailRef.value.refresh()
- }
- getList()
- })
- }
- }
- const saveUser = () => {
- if (!form.stationId) return ElMessage.warning('所属站点只能选择具体的站点')
- if (!form.name) return ElMessage.warning('请输入姓名')
- if (!form.phone) return ElMessage.warning('请输入电话')
- if (!form.address) return ElMessage.warning('请输入详细住址')
- if (!form.entryMethod) return ElMessage.warning('请选择入门方式')
- if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入开门密码')
- if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙位置')
- submitLoading.value = true
- form.tagIds = selectedTagIds.value
- if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
- form.regionCode = regionCascaderValue.value.join('/')
- } else {
- form.regionCode = ''
- }
- const api = isEdit.value ? updateCustomer(form) : addCustomer(form)
- api.then(() => {
- ElMessage.success('保存成功')
- dialogVisible.value = false
- getList()
- }).finally(() => {
- submitLoading.value = false
- })
- }
- const handleDelete = (row) => {
- ElMessageBox.confirm('确认删除该用户档案吗?', '提示', { type: 'warning' }).then(() => {
- delCustomer(row.id).then(() => {
- ElMessage.success('删除成功')
- getList()
- })
- })
- }
- const handleStatusChange = (row) => {
- const statusText = row.status === 0 ? '启用' : '停用'
- ElMessageBox.confirm(`确认${statusText}用户 "${row.name}" 吗?`, '提示', { type: 'warning' }).then(() => {
- changeCustomerStatus(row.id, row.status).then(() => {
- ElMessage.success(`${statusText}成功`)
- })
- }).catch(() => {
- row.status = row.status === 0 ? 1 : 0
- })
- }
- const handleCommand = (command, row) => {
- if (command === 'remark') {
- handleRemark(row)
- } else if (command === 'delete') {
- handleDelete(row)
- }
- }
- const baseUrl = import.meta.env.VITE_APP_BASE_API
- const uploadUrl = baseUrl + '/resource/oss/upload'
- const handleUserUploadFile = async (file) => {
- const formData = new FormData()
- formData.append('file', file.raw)
- try {
- const headers = globalHeaders()
- const res = await fetch(uploadUrl, {
- method: 'POST',
- headers: {
- 'Authorization': headers.Authorization,
- 'clientid': headers.clientid
- },
- body: formData
- })
- const result = await res.json()
- if (result.code === 200) {
- form.avatar = result.data.ossId
- userAvatarDisplayUrl.value = result.data.url
- } else {
- ElMessage.error(result.msg || '头像上传失败')
- }
- } catch (e) {
- ElMessage.error('头像上传失败')
- }
- }
- const handlePetUploadFile = async (file) => {
- const formData = new FormData()
- formData.append('file', file.raw)
- try {
- const headers = globalHeaders()
- const res = await fetch(uploadUrl, {
- method: 'POST',
- headers: {
- 'Authorization': headers.Authorization,
- 'clientid': headers.clientid
- },
- body: formData
- })
- const result = await res.json()
- if (result.code === 200) {
- petForm.avatar = result.data.ossId
- petAvatarDisplayUrl.value = result.data.url
- } else {
- ElMessage.error(result.msg || '头像上传失败')
- }
- } catch (e) {
- ElMessage.error('头像上传失败')
- }
- }
- const handlePetUploadVaccineCert = async (file) => {
- const formData = new FormData()
- formData.append('file', file.raw)
- try {
- const headers = globalHeaders()
- const res = await fetch(uploadUrl, {
- method: 'POST',
- headers: {
- 'Authorization': headers.Authorization,
- 'clientid': headers.clientid
- },
- body: formData
- })
- const result = await res.json()
- if (result.code === 200) {
- petForm.vaccineCert = result.data.ossId
- petVaccineCertDisplayUrl.value = result.data.url
- } else {
- ElMessage.error(result.msg || '疫苗凭证上传失败')
- }
- } catch (e) {
- ElMessage.error('疫苗凭证上传失败')
- }
- }
- const openAddPet = () => {
- petDialogActiveTab.value = 'basic'
- Object.assign(petForm, {
- id: undefined, userId: currentCustomerId.value, avatar: undefined, name: '', type: 0, gender: undefined,
- breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
- arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
- personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
- vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
- })
- petAvatarDisplayUrl.value = ''
- petVaccineCertDisplayUrl.value = ''
- petDialogVisible.value = true
- }
- const handlePetDetail = (row) => {
- selectedPetId.value = row.id;
- petDrawerVisible.value = true;
- }
- const handlePetEdit = (row) => {
- petDialogActiveTab.value = 'basic'
- Object.assign(petForm, {
- id: row.id, userId: row.userId, avatar: row.avatar, name: row.name, type: row.type,
- gender: row.gender, breed: row.breed, birthday: row.birthday, age: row.age,
- weight: row.weight, size: row.size, isSterilized: row.isSterilized,
- arrivalTime: row.arrivalTime, houseType: row.houseType, entryMethod: row.entryMethod,
- entryPassword: row.entryPassword, keyLocation: row.keyLocation,
- personality: row.personality, cutePersonality: row.cutePersonality,
- healthStatus: row.healthStatus, aggression: row.aggression,
- vaccineStatus: row.vaccineStatus, vaccineCert: row.vaccineCert,
- medicalHistory: row.medicalHistory, allergies: row.allergies, remark: row.remark,
- tagIds: row.tags ? row.tags.map(t => t.id) : []
- })
- petAvatarDisplayUrl.value = row.avatarUrl || ''
- petVaccineCertDisplayUrl.value = row.vaccineCertUrl || ''
- petDialogVisible.value = true
- }
- const handlePetRemark = (row) => {
- currentUser.value = row
- remarkTargetType.value = 'pet'
- remarkForm.content = row.remark || ''
- remarkDialogVisible.value = true
- }
- const handlePetDelete = (row) => {
- ElMessageBox.confirm(`确认删除宠物 [${row.name}] 吗?`, '提示', { type: 'warning' }).then(() => {
- delPet(row.id).then(() => {
- ElMessage.success('宠物删除成功')
- customerDetailRef.value.refresh()
- getList()
- })
- })
- }
- const savePet = () => {
- if (!petForm.name) return ElMessage.warning('请输入宠物姓名');
- if (!petForm.breed) return ElMessage.warning('请选择品种');
- if (!petForm.size) return ElMessage.warning('请选择体型');
- if (petForm.weight === undefined || petForm.weight === null) return ElMessage.warning('请输入体重(kg)');
- if (petForm.age === undefined || petForm.age === null) return ElMessage.warning('请输入年龄(岁)');
- if (!petForm.houseType) return ElMessage.warning('请选择家庭房屋类型');
- if (!petForm.entryMethod) return ElMessage.warning('请选择入门方式');
- if (petForm.entryMethod === 'password' && !petForm.entryPassword) return ElMessage.warning('请输入门锁密码');
- if (petForm.entryMethod === 'key' && !petForm.keyLocation) return ElMessage.warning('请输入钥匙存放位置');
- if (!petForm.healthStatus) return ElMessage.warning('请选择健康状态');
- if (petForm.aggression === undefined || petForm.aggression === null) return ElMessage.warning('请选择是否有攻击倾向');
- if (!petForm.vaccineStatus) return ElMessage.warning('请选择疫苗情况');
- if (!petForm.medicalHistory) return ElMessage.warning('请输入既往病史');
- if (!petForm.allergies) return ElMessage.warning('请输入过敏史');
- submitLoading.value = true
- const data = { ...petForm, aggression: Number(petForm.aggression) || 0 }
- const api = data.id ? updatePet(data) : addPet(data)
- api.then(() => {
- ElMessage.success('宠物档案保存成功')
- petDialogVisible.value = false
- customerDetailRef.value.refresh()
- getList()
- }).finally(() => {
- submitLoading.value = false
- })
- }
- onMounted(() => {
- getList()
- loadTags()
- loadAreaStation()
- getBrandList()
- })
- </script>
- <style scoped>
- .page-container { padding: 20px; }
- .card-header { display: flex; justify-content: space-between; align-items: center; }
- .title { font-weight: bold; }
- .profile-header {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- padding-bottom: 20px;
- border-bottom: 1px solid #f0f0f0;
- }
- .profile-basic {
- margin-left: 20px;
- }
- .name-row {
- display: flex;
- align-items: center;
- }
- .name {
- font-size: 20px;
- font-weight: bold;
- color: #303133;
- }
- .phone {
- margin-left: 10px;
- color: #666;
- }
- .section-title {
- font-size: 16px;
- font-weight: bold;
- margin-bottom: 15px;
- border-left: 4px solid #409EFF;
- padding-left: 10px;
- line-height: 1.2;
- }
- .form-section-header {
- font-weight: bold;
- margin-bottom: 15px;
- margin-top: 10px;
- padding-bottom: 5px;
- border-bottom: 1px dashed #eee;
- color: #303133;
- }
- .upload-avatar:hover {
- cursor: pointer;
- opacity: 0.8;
- }
- .pagination-container {
- margin-top: 20px;
- display: flex;
- justify-content: flex-end;
- }
- .customer-tabs {
- margin-bottom: 20px;
- background-color: #fff;
- padding: 10px 20px 0;
- border-radius: 4px;
- }
- :deep(.el-tabs__header) {
- margin-bottom: 0;
- }
- :deep(.el-tabs__nav-wrap::after) {
- height: 0;
- }
- /* Add Upload Styles */
- .avatar-uploader .el-upload {
- cursor: pointer;
- position: relative;
- overflow: hidden;
- display: inline-block;
- }
- .avatar-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 100px;
- height: 100px;
- text-align: center;
- border: 1px dashed #dcdfe6;
- border-radius: 4px;
- display: flex;
- justify-content: center;
- align-items: center;
- transition: .2s;
- }
- .avatar-uploader-icon:hover {
- border-color: #409EFF;
- color: #409EFF;
- }
- .avatar {
- width: 100px;
- height: 100px;
- display: block;
- border-radius: 4px;
- }
- </style>
|