|
|
@@ -5,46 +5,46 @@
|
|
|
<div class="card-header">
|
|
|
<span class="title">用户管理</span>
|
|
|
<div class="header-actions">
|
|
|
- <el-select v-model="searchForm.area" placeholder="所属区域" style="width: 150px; margin-right: 10px" clearable>
|
|
|
- <el-option label="朝阳区" value="朝阳区" />
|
|
|
- <el-option label="海淀区" value="海淀区" />
|
|
|
- <el-option label="浦东新区" value="浦东新区" />
|
|
|
+ <el-select v-model="searchForm.areaId" placeholder="所属区域" style="width: 150px; margin-right: 10px" clearable @change="onSearchAreaChange">
|
|
|
+ <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
|
|
|
</el-select>
|
|
|
- <el-select v-model="searchForm.station" placeholder="所属站点" style="width: 150px; margin-right: 10px" clearable>
|
|
|
- <el-option label="三里屯服务站" value="三里屯服务站" />
|
|
|
- <el-option label="中关村服务站" value="中关村服务站" />
|
|
|
+ <el-select v-model="searchForm.stationId" placeholder="所属站点" style="width: 150px; margin-right: 10px" clearable @change="handleSearch">
|
|
|
+ <el-option v-for="station in filteredStationList" :key="station.id" :label="station.name" :value="station.id" />
|
|
|
</el-select>
|
|
|
- <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px;" clearable />
|
|
|
+ <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">新增用户</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
- <el-table :data="filteredTableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
|
|
|
+ <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.avatar" style="margin-right: 10px;" />
|
|
|
<div>
|
|
|
<div style="font-weight: bold;">{{ scope.row.name }}
|
|
|
- <el-icon v-if="scope.row.gender === '女'" color="#F56C6C"><Female /></el-icon>
|
|
|
- <el-icon v-else color="#409EFF"><Male /></el-icon>
|
|
|
+ <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 prop="address" label="住址" show-overflow-tooltip min-width="150" />
|
|
|
+ <el-table-column label="住址" show-overflow-tooltip min-width="150">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ [scope.row.regionCode ? scope.row.regionCode.replace(/\//g, ' ') : '', 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.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px;">{{ tag.name }}</el-tag>
|
|
|
+ <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><el-tag size="small" effect="plain" :type="scope.row.source.includes('平台') ? '' : 'warning'">{{ scope.row.source }}</el-tag></div>
|
|
|
- <div style="font-size: 12px; color: #999; margin-top: 4px;">Created: {{ scope.row.entryTime }}</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">
|
|
|
@@ -61,6 +61,8 @@
|
|
|
<template #default="scope">
|
|
|
<el-switch
|
|
|
v-model="scope.row.status"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
inline-prompt
|
|
|
active-text="正常"
|
|
|
inactive-text="停用"
|
|
|
@@ -92,13 +94,13 @@
|
|
|
</el-table>
|
|
|
<div class="pagination-container">
|
|
|
<el-pagination
|
|
|
- v-model:current-page="currentPage"
|
|
|
- v-model:page-size="pageSize"
|
|
|
+ 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="handleSizeChange"
|
|
|
- @current-change="handleCurrentChange"
|
|
|
+ @size-change="getList"
|
|
|
+ @current-change="getList"
|
|
|
/>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
@@ -110,13 +112,11 @@
|
|
|
<div class="profile-basic">
|
|
|
<div class="name-row">
|
|
|
<span class="name">{{ currentUser.name }}</span>
|
|
|
- <el-tag size="small" :type="currentUser.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
|
|
|
- {{ currentUser.gender }}
|
|
|
- </el-tag>
|
|
|
+ <dict-tag :options="sys_user_sex" :value="currentUser.gender" />
|
|
|
<span class="phone">{{ currentUser.phone }}</span>
|
|
|
</div>
|
|
|
<div class="tags-row" style="margin-top: 8px">
|
|
|
- <el-tag v-for="tag in currentUser.tags" :key="tag.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px">
|
|
|
+ <el-tag v-for="tag in currentUser.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
|
|
|
{{ tag.name }}
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
@@ -139,10 +139,10 @@
|
|
|
<el-descriptions :column="2" border>
|
|
|
<el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="房屋类型">
|
|
|
- {{ currentUser.houseType === 'stairs' ? '楼梯' : '电梯' }}
|
|
|
+ <dict-tag :options="sys_house_type" :value="currentUser.houseType" />
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="入门方式">
|
|
|
- {{ currentUser.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
|
|
|
+ <dict-tag :options="sys_entry_method" :value="currentUser.entryMethod" />
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="开门详情" :span="2">
|
|
|
{{ currentUser.entryMethod === 'password' ? currentUser.entryPassword : currentUser.keyLocation }}
|
|
|
@@ -204,13 +204,13 @@
|
|
|
<el-tab-pane label="档案日志" name="logs">
|
|
|
<el-timeline style="margin-top: 10px; padding-left: 5px;">
|
|
|
<el-timeline-item
|
|
|
- v-for="(log, index) in mockLogs"
|
|
|
+ v-for="(log, index) in changeLogs"
|
|
|
:key="index"
|
|
|
- :timestamp="log.timestamp"
|
|
|
- :type="log.type"
|
|
|
+ :timestamp="log.createTime"
|
|
|
+ type="primary"
|
|
|
>
|
|
|
- {{ log.content }}
|
|
|
- <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operator }}</div>
|
|
|
+ [{{ log.changeType }}] {{ log.content }}
|
|
|
+ <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
|
|
|
</el-timeline-item>
|
|
|
</el-timeline>
|
|
|
</el-tab-pane>
|
|
|
@@ -239,9 +239,15 @@
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="所属区域">
|
|
|
- <el-select v-model="form.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
|
|
|
- <el-option label="朝阳区" value="朝阳区" />
|
|
|
- <el-option label="海淀区" value="海淀区" />
|
|
|
+ <el-select v-model="form.areaId" style="width: 100%" filterable placeholder="请选择区域" clearable @change="form.stationId = undefined">
|
|
|
+ <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="所属站点">
|
|
|
+ <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable>
|
|
|
+ <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -253,10 +259,9 @@
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="性别">
|
|
|
- <el-radio-group v-model="form.gender">
|
|
|
- <el-radio label="男">男</el-radio>
|
|
|
- <el-radio label="女">女</el-radio>
|
|
|
- </el-radio-group>
|
|
|
+ <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>
|
|
|
|
|
|
@@ -272,21 +277,19 @@
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
- <el-form-item label="详细住址"><el-input v-model="form.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
|
|
|
+ <el-form-item label="详细住址"><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 label="stairs">楼梯</el-radio>
|
|
|
- <el-radio label="elevator">电梯</el-radio>
|
|
|
+ <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="入门方式">
|
|
|
<el-radio-group v-model="form.entryMethod">
|
|
|
- <el-radio label="password">密码开门</el-radio>
|
|
|
- <el-radio label="key">钥匙开门</el-radio>
|
|
|
+ <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>
|
|
|
@@ -306,7 +309,7 @@
|
|
|
<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.type" effect="light" size="small">{{ tag.name }}</el-tag>
|
|
|
+ <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
|
|
|
</el-option>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
@@ -318,7 +321,7 @@
|
|
|
</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" @click="saveUser" 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>
|
|
|
|
|
|
@@ -356,16 +359,15 @@
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="性别">
|
|
|
- <el-radio-group v-model="petForm.gender">
|
|
|
- <el-radio label="公">公</el-radio>
|
|
|
- <el-radio label="母">母</el-radio>
|
|
|
- </el-radio-group>
|
|
|
+ <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="品种">
|
|
|
<el-select v-model="petForm.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
|
|
|
- <el-option v-for="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
|
|
|
+ <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>
|
|
|
@@ -392,9 +394,9 @@
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="宠物标签">
|
|
|
- <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width: 100%">
|
|
|
- <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
|
|
|
- <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
|
|
|
+ <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>
|
|
|
@@ -437,7 +439,7 @@
|
|
|
</el-radio-group>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="是否有攻击倾向">
|
|
|
- <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
|
|
|
+ <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="疫苗情况">
|
|
|
<el-radio-group v-model="petForm.vaccine">
|
|
|
@@ -465,7 +467,7 @@
|
|
|
<template #footer>
|
|
|
<span class="dialog-footer">
|
|
|
<el-button @click="petDialogVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="savePet">保存</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="savePet">保存</el-button>
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
@@ -473,24 +475,66 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, computed } from 'vue'
|
|
|
+import { ref, reactive, computed, onMounted, getCurrentInstance, toRefs } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, changeCustomerStatus } from '@/api/archieves/customer'
|
|
|
+import { listAllTag } from '@/api/archieves/tag'
|
|
|
+import { listPetByUser, addPet, updatePet, delPet } from '@/api/archieves/pet'
|
|
|
+import { listAllChangeLog } from '@/api/archieves/changeLog'
|
|
|
+import { listOnStore } from '@/api/system/areaStation'
|
|
|
+
|
|
|
+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 areaList = computed(() => allNodes.value.filter(n => n.type === 1))
|
|
|
+const filteredStationList = computed(() => {
|
|
|
+ const areaId = searchForm.areaId
|
|
|
+ const stations = allNodes.value.filter(n => n.type === 2)
|
|
|
+ if (areaId) {
|
|
|
+ return stations.filter(s => s.parentId === areaId)
|
|
|
+ }
|
|
|
+ return stations
|
|
|
+})
|
|
|
+const formStationList = computed(() => {
|
|
|
+ const areaId = form.areaId
|
|
|
+ const stations = allNodes.value.filter(n => n.type === 2)
|
|
|
+ if (areaId) {
|
|
|
+ return stations.filter(s => s.parentId === areaId)
|
|
|
+ }
|
|
|
+ return stations
|
|
|
+})
|
|
|
|
|
|
-const currentPage = ref(1)
|
|
|
-const pageSize = ref(10)
|
|
|
-const total = ref(100)
|
|
|
-
|
|
|
-const handleSizeChange = (val) => {
|
|
|
- console.log(`每页 ${val} 条`)
|
|
|
+const loadAreaStation = () => {
|
|
|
+ listOnStore().then((res) => {
|
|
|
+ allNodes.value = res.data || []
|
|
|
+ })
|
|
|
}
|
|
|
-const handleCurrentChange = (val) => {
|
|
|
- console.log(`当前页: ${val}`)
|
|
|
+
|
|
|
+const onSearchAreaChange = () => {
|
|
|
+ searchForm.stationId = undefined
|
|
|
+ handleSearch()
|
|
|
}
|
|
|
|
|
|
+const queryParams = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ keyword: '',
|
|
|
+ areaId: undefined,
|
|
|
+ stationId: undefined,
|
|
|
+ status: undefined
|
|
|
+})
|
|
|
+
|
|
|
const searchForm = reactive({
|
|
|
keyword: '',
|
|
|
- area: '',
|
|
|
- station: ''
|
|
|
+ areaId: undefined,
|
|
|
+ stationId: undefined
|
|
|
})
|
|
|
|
|
|
const dialogVisible = ref(false)
|
|
|
@@ -504,192 +548,171 @@ const petDialogActiveTab = ref('basic')
|
|
|
const selectedTagIds = ref([])
|
|
|
const currentUser = ref({})
|
|
|
const currentPets = ref([])
|
|
|
+const tableData = ref([])
|
|
|
+
|
|
|
+const allUserTags = ref([])
|
|
|
+const allPetTags = ref([])
|
|
|
+const changeLogs = ref([])
|
|
|
|
|
|
-// Mock Data
|
|
|
-const allUserTags = [
|
|
|
- { id: 1, name: '优质客户', type: 'success' },
|
|
|
- { id: 2, name: '潜在流失', type: 'warning' },
|
|
|
- { id: 3, name: '黑名单', type: 'danger' }
|
|
|
-]
|
|
|
-
|
|
|
-const petBreeds = [
|
|
|
- '金毛', '拉布拉多', '柴犬', '柯基', '哈士奇', '阿拉斯加', '萨摩耶', '边境牧羊犬', '德国牧羊犬', '贵宾犬/泰迪', '比熊', '博美', '雪纳瑞', '法斗', '中华田园犬',
|
|
|
- '英短', '美短', '布偶猫', '加菲猫', '暹罗猫', '波斯猫', '缅因猫', '中华田园猫'
|
|
|
-]
|
|
|
-
|
|
|
-const allPetTags = [
|
|
|
- { name: '易过敏', type: 'danger' },
|
|
|
- { name: '胆小', type: 'warning' },
|
|
|
- { name: '攻击性', type: 'info' },
|
|
|
- { name: '粘人', type: 'success' }
|
|
|
-]
|
|
|
-
|
|
|
-const tableData = ref([
|
|
|
- {
|
|
|
- id: 101,
|
|
|
- avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
- name: '张先生',
|
|
|
- phone: '13800138000',
|
|
|
- gender: '男',
|
|
|
- address: '北京市朝阳区三里屯',
|
|
|
- houseType: 'elevator',
|
|
|
- entryMethod: 'password',
|
|
|
- entryPassword: '456',
|
|
|
- keyLocation: '',
|
|
|
- remark: '经常周末来',
|
|
|
- tags: [{ name: '优质客户', type: 'success' }],
|
|
|
- petCount: 2,
|
|
|
- entryTime: '2025-01-15 10:00:00',
|
|
|
- source: '平台录入',
|
|
|
- orderCount: 12,
|
|
|
- totalAmount: 3580.00,
|
|
|
- area: '朝阳区',
|
|
|
- station: '三里屯服务站',
|
|
|
- status: true
|
|
|
- },
|
|
|
- {
|
|
|
- id: 102,
|
|
|
- avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
- name: '李小姐',
|
|
|
- phone: '13900139000',
|
|
|
- gender: '女',
|
|
|
- address: '上海市浦东新区',
|
|
|
- houseType: 'stairs',
|
|
|
- entryMethod: 'key',
|
|
|
- entryPassword: '',
|
|
|
- keyLocation: '门口地垫下',
|
|
|
- remark: '',
|
|
|
- tags: [],
|
|
|
- petCount: 0,
|
|
|
- entryTime: '2025-02-01 14:30:00',
|
|
|
- source: '萌它宠物连锁录入',
|
|
|
- orderCount: 0,
|
|
|
- totalAmount: 0.00,
|
|
|
- area: '浦东新区',
|
|
|
- station: '',
|
|
|
- status: true
|
|
|
- }
|
|
|
-])
|
|
|
|
|
|
const mockOrders = ref([
|
|
|
{ orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
|
|
|
{ orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
|
|
|
])
|
|
|
|
|
|
-const mockLogs = ref([
|
|
|
- { content: '用户注册成功', timestamp: '2025-01-15 10:00:00', operator: '系统', type: 'success' },
|
|
|
- { content: '新增宠物档案 [旺财]', timestamp: '2025-01-15 10:05:00', operator: '张先生', type: 'primary' }
|
|
|
-])
|
|
|
-
|
|
|
const form = reactive({
|
|
|
- id: null,
|
|
|
- avatar: '',
|
|
|
+ id: undefined,
|
|
|
name: '',
|
|
|
phone: '',
|
|
|
- gender: '男',
|
|
|
- address: '',
|
|
|
- detailAddress: '',
|
|
|
+ avatar: undefined,
|
|
|
+ gender: 0,
|
|
|
+ birthday: '',
|
|
|
+ idCard: '',
|
|
|
+ areaId: undefined,
|
|
|
+ stationId: undefined,
|
|
|
+ regionCode: '',
|
|
|
region: [],
|
|
|
- houseType: 'elevator',
|
|
|
- entryMethod: 'password',
|
|
|
+ address: '',
|
|
|
+ houseType: '',
|
|
|
+ entryMethod: '',
|
|
|
entryPassword: '',
|
|
|
keyLocation: '',
|
|
|
+ source: '',
|
|
|
+ emergencyContact: '',
|
|
|
+ emergencyPhone: '',
|
|
|
+ memberLevel: 0,
|
|
|
+ status: 0,
|
|
|
remark: '',
|
|
|
- source: '平台录入',
|
|
|
- entryTime: '',
|
|
|
- area: '',
|
|
|
- status: true
|
|
|
+ tagIds: []
|
|
|
})
|
|
|
|
|
|
const petForm = reactive({
|
|
|
- id: null,
|
|
|
- avatar: '',
|
|
|
+ id: undefined,
|
|
|
+ userId: undefined,
|
|
|
+ avatar: undefined,
|
|
|
name: '',
|
|
|
- gender: '公',
|
|
|
+ type: 0,
|
|
|
+ gender: 0,
|
|
|
breed: '',
|
|
|
+ birthday: '',
|
|
|
age: 1,
|
|
|
- size: 'small',
|
|
|
weight: 5,
|
|
|
- personality: '',
|
|
|
- cutePersonality: '',
|
|
|
- tags: [],
|
|
|
-
|
|
|
+ size: 'small',
|
|
|
+ isSterilized: 0,
|
|
|
arrivalTime: '',
|
|
|
- houseType: 'stairs',
|
|
|
- entryMethod: 'key',
|
|
|
+ houseType: '',
|
|
|
+ entryMethod: '',
|
|
|
entryPassword: '',
|
|
|
keyLocation: '',
|
|
|
-
|
|
|
- healthStatus: '健康',
|
|
|
- aggression: false,
|
|
|
- vaccine: '无',
|
|
|
- vaccineCert: '',
|
|
|
+ personality: '',
|
|
|
+ cutePersonality: '',
|
|
|
+ healthStatus: '',
|
|
|
+ aggression: 0,
|
|
|
+ vaccineStatus: '',
|
|
|
+ vaccineCert: undefined,
|
|
|
medicalHistory: '',
|
|
|
- allergies: ''
|
|
|
+ allergies: '',
|
|
|
+ remark: '',
|
|
|
+ tagIds: []
|
|
|
})
|
|
|
|
|
|
const remarkForm = reactive({ content: '' })
|
|
|
|
|
|
-const filteredTableData = computed(() => {
|
|
|
- return tableData.value.filter(item => {
|
|
|
- const matchKey = !searchForm.keyword || item.name.includes(searchForm.keyword) || item.phone.includes(searchForm.keyword)
|
|
|
- const matchArea = !searchForm.area || item.area === searchForm.area
|
|
|
- const matchStation = !searchForm.station || item.station === searchForm.station
|
|
|
- return matchKey && matchArea && matchStation
|
|
|
- })
|
|
|
+const pcaOptions = computed(() => {
|
|
|
+ const cities = allNodes.value.filter(n => n.type === 0)
|
|
|
+ return cities.map(city => ({
|
|
|
+ value: city.name,
|
|
|
+ label: city.name,
|
|
|
+ children: allNodes.value
|
|
|
+ .filter(n => n.type === 1 && n.parentId === city.id)
|
|
|
+ .map(district => ({
|
|
|
+ value: district.name,
|
|
|
+ label: district.name
|
|
|
+ }))
|
|
|
+ }))
|
|
|
})
|
|
|
|
|
|
+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: 'user', 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: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
|
|
|
- houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
|
|
|
- source: '平台录入', entryTime: new Date().toLocaleString().replace(/\//g, '-'), area: '', status: true
|
|
|
+ id: undefined, name: '', phone: '', avatar: undefined, gender: 0, birthday: '', idCard: '',
|
|
|
+ areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
|
|
|
+ houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
|
|
|
+ emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
|
|
|
})
|
|
|
dialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
const handleEdit = (row) => {
|
|
|
isEdit.value = true
|
|
|
- Object.assign(form, row)
|
|
|
- // Mock parsing address to region? For now just keep address string
|
|
|
- form.detailAddress = row.address // Simplify: edit mode just show full string in detail
|
|
|
- form.region = []
|
|
|
+ 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, source: data.source,
|
|
|
+ emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
|
|
|
+ memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
|
|
|
+ })
|
|
|
+ selectedTagIds.value = data.tags ? data.tags.map(t => t.id) : []
|
|
|
+ dialogVisible.value = true
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- selectedTagIds.value = row.tags.map(t => allUserTags.find(at => at.name === t.name)?.id).filter(id => id)
|
|
|
- dialogVisible.value = true
|
|
|
+const handleDetail = (row) => {
|
|
|
+ getCustomer(row.id).then((res) => {
|
|
|
+ currentUser.value = res.data
|
|
|
+ detailActiveTab.value = 'info'
|
|
|
+ loadDetailPets(row.id)
|
|
|
+ loadDetailLogs(row.id, 'customer')
|
|
|
+ drawerVisible.value = true
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-// Mock PCA Data
|
|
|
-const pcaOptions = [
|
|
|
- {
|
|
|
- value: '北京市', label: '北京市',
|
|
|
- children: [
|
|
|
- { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- value: '上海市', label: '上海市',
|
|
|
- children: [
|
|
|
- { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
|
|
|
- ]
|
|
|
- }
|
|
|
-]
|
|
|
+const loadDetailPets = (userId) => {
|
|
|
+ listPetByUser(userId).then((res) => {
|
|
|
+ currentPets.value = res.data || []
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
-const handleDetail = (row) => {
|
|
|
- currentUser.value = { ...row }
|
|
|
- detailActiveTab.value = 'info'
|
|
|
- // Mock Load Pets
|
|
|
- if(row.petCount > 0) {
|
|
|
- currentPets.value = [
|
|
|
- { id: 1, name: '旺财', breed: '金毛', gender: '公', age: 3, status: '健康', vaccine: '已打3次' },
|
|
|
- { id: 2, name: '咪咪', breed: '加菲猫', gender: '母', age: 2, status: '健康', vaccine: '无' }
|
|
|
- ].slice(0, row.petCount)
|
|
|
- } else {
|
|
|
- currentPets.value = []
|
|
|
- }
|
|
|
- drawerVisible.value = true
|
|
|
+const loadDetailLogs = (targetId, targetType) => {
|
|
|
+ listAllChangeLog(targetId, targetType).then((res) => {
|
|
|
+ changeLogs.value = res.data || []
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
const handleRemark = (row) => {
|
|
|
@@ -700,51 +723,69 @@ const handleRemark = (row) => {
|
|
|
|
|
|
const saveRemark = () => {
|
|
|
if (!remarkForm.content) return ElMessage.warning('请输入内容')
|
|
|
- mockLogs.value.unshift({
|
|
|
- content: remarkForm.content,
|
|
|
- timestamp: new Date().toLocaleString(),
|
|
|
- operator: '管理员',
|
|
|
- type: 'warning'
|
|
|
+ // For now, update customer remark via API
|
|
|
+ const data = { id: currentUser.value.id, remark: remarkForm.content }
|
|
|
+ updateCustomer(data).then(() => {
|
|
|
+ ElMessage.success('备注添加成功')
|
|
|
+ remarkDialogVisible.value = false
|
|
|
+ getList()
|
|
|
})
|
|
|
- ElMessage.success('备注添加成功')
|
|
|
- remarkDialogVisible.value = false
|
|
|
}
|
|
|
|
|
|
const saveUser = () => {
|
|
|
if (!form.name) return ElMessage.warning('请输入姓名')
|
|
|
-
|
|
|
- const newTags = selectedTagIds.value.map(id => allUserTags.find(t => t.id === id))
|
|
|
-
|
|
|
- const fullAddress = (form.region ? form.region.join('') : '') + form.detailAddress
|
|
|
- const saveForm = { ...form, address: fullAddress }
|
|
|
-
|
|
|
- if (isEdit.value) {
|
|
|
- const idx = tableData.value.findIndex(item => item.id === form.id)
|
|
|
- if (idx !== -1) Object.assign(tableData.value[idx], { ...saveForm, tags: newTags })
|
|
|
+ if (!form.phone) return ElMessage.warning('请输入电话')
|
|
|
+ submitLoading.value = true
|
|
|
+ form.tagIds = selectedTagIds.value
|
|
|
+ if (form.region && form.region.length > 0) {
|
|
|
+ form.regionCode = form.region.join('/')
|
|
|
} else {
|
|
|
- tableData.value.push({
|
|
|
- id: Date.now(),
|
|
|
- ...saveForm,
|
|
|
- // entryTime is auto set in handleAdd, or here if we want current time on save
|
|
|
- entryTime: new Date().toLocaleString().replace(/\//g, '-'),
|
|
|
- tags: newTags,
|
|
|
- avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
|
|
|
- petCount: 0,
|
|
|
- orderCount: 0,
|
|
|
- totalAmount: 0.00
|
|
|
- })
|
|
|
+ form.regionCode = ''
|
|
|
}
|
|
|
- ElMessage.success('保存成功')
|
|
|
- dialogVisible.value = false
|
|
|
+ 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(() => {
|
|
|
- tableData.value = tableData.value.filter(item => item.id !== row.id)
|
|
|
- ElMessage.success('删除成功')
|
|
|
+ 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)
|
|
|
+ } else if (command === 'enable') {
|
|
|
+ row.status = 0
|
|
|
+ handleStatusChange(row)
|
|
|
+ } else if (command === 'disable') {
|
|
|
+ row.status = 1
|
|
|
+ handleStatusChange(row)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const handlePetUploadFile = (file) => {
|
|
|
petForm.avatar = URL.createObjectURL(file.raw)
|
|
|
}
|
|
|
@@ -755,103 +796,71 @@ const handlePetUploadVaccineCert = (file) => {
|
|
|
const openAddPet = () => {
|
|
|
petDialogActiveTab.value = 'basic'
|
|
|
Object.assign(petForm, {
|
|
|
- id: null, avatar: '', name: '', gender: '公', breed: '', age: 1, size: 'small', weight: 5, ownerId: null,
|
|
|
- personality: '', cutePersonality: '', tags: [],
|
|
|
- arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
|
|
|
- healthStatus: '健康', aggression: false, vaccine: '无', vaccineCert: '', medicalHistory: '', allergies: ''
|
|
|
+ id: undefined, userId: currentUser.value.id, avatar: undefined, name: '', type: 0, gender: 0,
|
|
|
+ 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: []
|
|
|
})
|
|
|
petDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
const handlePetDetail = (row) => {
|
|
|
- // Show simple details or full? The dialog is editable, so detail view can just be the grid or a text overview.
|
|
|
- // For now, let's just make it show the edit dialog but read-only?
|
|
|
- // Or just alert like before? User didn't specify, but "功能复刻" implies full detail.
|
|
|
- // The "Pet Archive" has a Drawer for details. I should probably use that or just use the Edit Dialog for now as "Detailed View".
|
|
|
- // But since "handlePetEdit" exists, maybe "handlePetDetail" isn't fully implemented in UserList yet.
|
|
|
- // I will stick to existing alert or simple functionality unless asked.
|
|
|
ElMessage.info(`查看宠物 [${row.name}] 详情`)
|
|
|
}
|
|
|
|
|
|
const handlePetEdit = (row) => {
|
|
|
petDialogActiveTab.value = 'basic'
|
|
|
- const defaults = {
|
|
|
- avatar: '', name: '', gender: '公', breed: '', age: 1, size: 'small', weight: 5,
|
|
|
- personality: '', cutePersonality: '', tags: [],
|
|
|
- arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
|
|
|
- healthStatus: '健康', aggression: false, vaccine: '无', vaccineCert: '', medicalHistory: '', allergies: ''
|
|
|
- }
|
|
|
- Object.assign(petForm, { ...defaults, ...row })
|
|
|
+ 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) : []
|
|
|
+ })
|
|
|
petDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
const handlePetRemark = (row) => {
|
|
|
- // Reuse main remark dialog but maybe prefix content?
|
|
|
remarkForm.content = `[宠物:${row.name}] `
|
|
|
remarkDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
const handlePetDelete = (row) => {
|
|
|
ElMessageBox.confirm(`确认删除宠物 [${row.name}] 吗?`, '提示', { type: 'warning' }).then(() => {
|
|
|
- currentPets.value = currentPets.value.filter(p => p.id !== row.id)
|
|
|
- // Update counts
|
|
|
- if (currentUser.value.id) {
|
|
|
- const idx = tableData.value.findIndex(item => item.id === currentUser.value.id)
|
|
|
- if(idx !== -1) {
|
|
|
- tableData.value[idx].petCount = currentPets.value.length
|
|
|
- currentUser.value.petCount = currentPets.value.length
|
|
|
- }
|
|
|
- }
|
|
|
- ElMessage.success('宠物删除成功')
|
|
|
+ delPet(row.id).then(() => {
|
|
|
+ ElMessage.success('宠物删除成功')
|
|
|
+ loadDetailPets(currentUser.value.id)
|
|
|
+ getList()
|
|
|
+ })
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-const handleCommand = (command, row) => {
|
|
|
- if (command === 'remark') {
|
|
|
- handleRemark(row)
|
|
|
- } else if (command === 'delete') {
|
|
|
- handleDelete(row)
|
|
|
- } else if (command === 'enable' || command === 'disable') {
|
|
|
- // Toggle status status relies on row.status being updated or we update it manually here if not using v-model in switch
|
|
|
- // But since we use switch v-model in table, this command might be redundant if switch is there.
|
|
|
- // However, user asked for 'Operations can modify status'.
|
|
|
- // If we strictly follow 'Operations bar has Modify Status', then the Switch column is optional but good UX.
|
|
|
- // Let's assume the switch is the primary way, butdropdown item toggles it too.
|
|
|
- row.status = command === 'enable'
|
|
|
- handleStatusChange(row)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const handleStatusChange = (row) => {
|
|
|
- ElMessage.success(`${row.name} 已${row.status ? '启用' : '停用'}`)
|
|
|
-}
|
|
|
-
|
|
|
const savePet = () => {
|
|
|
- if(!petForm.name) return ElMessage.warning('请输入宠物昵称')
|
|
|
-
|
|
|
- if (petForm.id) {
|
|
|
- // Edit existing
|
|
|
- const idx = currentPets.value.findIndex(p => p.id === petForm.id)
|
|
|
- if (idx !== -1) Object.assign(currentPets.value[idx], petForm)
|
|
|
- } else {
|
|
|
- // Add new
|
|
|
- currentPets.value.push({
|
|
|
- id: Date.now(),
|
|
|
- ...petForm
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // Update main count
|
|
|
- if (currentUser.value.id) {
|
|
|
- const idx = tableData.value.findIndex(item => item.id === currentUser.value.id)
|
|
|
- if(idx !== -1) {
|
|
|
- tableData.value[idx].petCount = currentPets.value.length
|
|
|
- currentUser.value.petCount = currentPets.value.length
|
|
|
- }
|
|
|
- }
|
|
|
- ElMessage.success('宠物档案保存成功')
|
|
|
- petDialogVisible.value = false
|
|
|
+ if (!petForm.name) 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
|
|
|
+ loadDetailPets(currentUser.value.id)
|
|
|
+ getList()
|
|
|
+ }).finally(() => {
|
|
|
+ submitLoading.value = false
|
|
|
+ })
|
|
|
}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+ loadTags()
|
|
|
+ loadAreaStation()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|