index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. <template>
  2. <div class="p-2">
  3. <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
  4. :leave-active-class="proxy?.animate.searchAnimate.leave">
  5. <div v-show="showSearch" class="mb-[10px] bg-white p-[20px] rounded-[4px] flex justify-between items-center shadow-sm" style="background-color: #fff; padding: 20px; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08);">
  6. <div class="text-[18px] font-bold text-[#303133]" style="font-size: 18px; font-weight: bold; color: #303133;">门店管理</div>
  7. <div class="flex items-center gap-[10px]" style="display: flex; gap: 10px; align-items: center;">
  8. <el-input v-model="queryParams.storeOrContact" placeholder="搜索门店名称/联系人" prefix-icon="Search"
  9. style="width: 250px" clearable @keyup.enter="handleQuery" @clear="handleQuery" />
  10. <el-cascader v-model="searchRegionValue" :options="areaOptions" placeholder="所属城市"
  11. style="width: 150px" clearable @change="handleSearchAreaChange" />
  12. <el-select v-model="queryParams.station" placeholder="所属站点" style="width: 150px" clearable @change="handleQuery">
  13. <el-option v-for="site in searchSiteOptions" :key="site.value" :label="site.label" :value="site.value" />
  14. </el-select>
  15. <el-select v-model="queryParams.status" placeholder="状态" style="width: 120px" clearable @change="handleQuery">
  16. <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
  17. </el-select>
  18. <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:store:add']">新增门店</el-button>
  19. </div>
  20. </div>
  21. </transition>
  22. <el-card shadow="never">
  23. <el-table v-loading="loading" border :data="storeList" @selection-change="handleSelectionChange">
  24. <el-table-column type="selection" width="55" align="center" />
  25. <el-table-column label="门店信息" align="left" width="300">
  26. <template #default="scope">
  27. <div class="store-info" style="display: flex; align-items: center; gap: 10px;">
  28. <div class="store-logo">
  29. <image-preview :src="scope.row.logoUrl" :width="50" :height="50" />
  30. </div>
  31. <div class="store-details" style="display: flex; flex-direction: column; gap: 6px;">
  32. <div class="store-name" style="font-size: 14px; font-weight: 500; color: #303133;">{{ scope.row.name }}</div>
  33. <div class="store-categories" style="display: flex; gap: 6px;">
  34. <el-tag size="small" type="warning" effect="plain" v-if="scope.row.tenantName">{{ scope.row.tenantName }}</el-tag>
  35. <el-tag size="small" type="success" effect="plain" v-if="scope.row.tenantCatergoriesName">{{ scope.row.tenantCatergoriesName }}</el-tag>
  36. </div>
  37. </div>
  38. </div>
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="资质认证" align="center" width="100">
  42. <template #default="scope">
  43. <image-preview v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
  44. <span v-else>-</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="服务项目" align="center" width="200">
  48. <template #default="scope">
  49. <div class="services">
  50. <el-tag v-for="service in scope.row.services" :key="service" size="small"
  51. style="margin-right: 5px; margin-bottom: 5px">
  52. {{ getServiceName(service) }}
  53. </el-tag>
  54. </div>
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="归属站点" align="center" width="150">
  58. <template #default="scope">
  59. <div>{{ scope.row.siteName }}</div>
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="服务单" align="center" width="150">
  63. <template #default="scope">
  64. <div>{{ scope.row.serviceOrder }}</div>
  65. </template>
  66. </el-table-column>
  67. <el-table-column label="营业时间" align="center" width="150">
  68. <template #default="scope">
  69. <div>{{ formatTime(scope.row.startBusinessTime) }}-{{ formatTime(scope.row.endBusinessTime) }}</div>
  70. </template>
  71. </el-table-column>
  72. <el-table-column label="门店地址" align="center" width="300">
  73. <template #default="scope">
  74. <div>{{ scope.row.detailAddress }}</div>
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="联系方式" align="left" width="180">
  78. <template #default="scope">
  79. <div style="display: flex; flex-direction: column; gap: 6px;">
  80. <div style="display: flex; align-items: center; gap: 6px; color: #606266; font-size: 14px;">
  81. <el-icon size="16"><User /></el-icon>
  82. <span>{{ scope.row.contact }}</span>
  83. </div>
  84. <div style="display: flex; align-items: center; gap: 6px; color: #409eff; font-size: 14px;">
  85. <el-icon size="16"><Phone /></el-icon>
  86. <span>{{ scope.row.contactNumber }}</span>
  87. </div>
  88. </div>
  89. </template>
  90. </el-table-column>
  91. <el-table-column label="有效期至" align="center" width="120">
  92. <template #default="scope">
  93. <div>{{ parseTime(scope.row.validity, '{y}-{m}-{d}') }}</div>
  94. </template>
  95. </el-table-column>
  96. <el-table-column label="状态" align="center" width="100">
  97. <template #default="scope">
  98. <template v-for="item in statusList" :key="item.value">
  99. <el-tag v-if="scope.row.status === item.value" :type="item.style">{{ item.label }}</el-tag>
  100. </template>
  101. </template>
  102. </el-table-column>
  103. <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
  104. <template #default="scope">
  105. <el-tooltip content="详情" placement="top">
  106. <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
  107. </el-tooltip>
  108. <el-tooltip content="修改" placement="top">
  109. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
  110. v-hasPermi="['system:store:edit']"></el-button>
  111. </el-tooltip>
  112. <el-tooltip content="更多" placement="top">
  113. <el-button link type="primary" icon="More"></el-button>
  114. </el-tooltip>
  115. </template>
  116. </el-table-column>
  117. </el-table>
  118. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
  119. v-model:limit="queryParams.pageSize" @pagination="getList" />
  120. </el-card>
  121. <!-- 添加或修改门店管理对话框 -->
  122. <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
  123. <el-form ref="storeFormRef" :model="form" :rules="rules" label-width="120px">
  124. <el-form-item label="门店Logo" prop="logo">
  125. <image-upload v-model="form.logo" :limit="1" />
  126. </el-form-item>
  127. <el-form-item label="营业执照" prop="businessLicense">
  128. <image-upload v-model="form.businessLicense" :limit="1" />
  129. </el-form-item>
  130. <el-form-item label="门店名称" prop="name">
  131. <el-input v-model="form.name" placeholder="请输入门店名称" />
  132. </el-form-item>
  133. <el-form-item label="服务项目" prop="services">
  134. <el-checkbox-group v-model="form.services">
  135. <el-checkbox v-for="service in serviceList" :key="service.id" :label="service.id" border>
  136. {{ service.name }}
  137. </el-checkbox>
  138. </el-checkbox-group>
  139. </el-form-item>
  140. <el-form-item label="商户分类" prop="tenantCatergories">
  141. <PageSelect v-model="form.tenantCatergories"
  142. :options="tenantCategoriesList.map(item => ({ value: item.id, label: item.name }))"
  143. :total="tenantCategoriesTotal" :pageSize="10" placeholder="请选择商户分类"
  144. @page-change="handleTenantCategoriesPageChange" @visible-change="handleTenantCategoriesVisibleChange" />
  145. </el-form-item>
  146. <el-form-item label="所属品牌" prop="tenantId">
  147. <PageSelect v-model="form.tenantId"
  148. :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
  149. :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
  150. @visible-change="handleBrandSelectVisibleChange" />
  151. </el-form-item>
  152. <el-form-item label="营业时间" prop="startBusinessTime">
  153. <el-row :gutter="10">
  154. <el-col :span="10">
  155. <el-time-picker clearable v-model="form.startBusinessTime" value-format="HH:mm" placeholder="开始时间"
  156. style="width: 100%">
  157. </el-time-picker>
  158. </el-col>
  159. <el-col :span="4" style="text-align: center; line-height: 40px">
  160. </el-col>
  161. <el-col :span="10">
  162. <el-time-picker clearable v-model="form.endBusinessTime" value-format="HH:mm" placeholder="结束时间"
  163. style="width: 100%">
  164. </el-time-picker>
  165. </el-col>
  166. </el-row>
  167. </el-form-item>
  168. <el-form-item label="联系人" prop="contact">
  169. <el-input v-model="form.contact" placeholder="请输入联系人" />
  170. </el-form-item>
  171. <el-form-item label="联系电话" prop="contactNumber">
  172. <el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
  173. </el-form-item>
  174. <el-form-item label="有效期至" prop="validity">
  175. <el-date-picker clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至"
  176. style="width: 100%">
  177. </el-date-picker>
  178. </el-form-item>
  179. <el-row :gutter="10">
  180. <el-col :span="12">
  181. <el-form-item label="所在区域" prop="regionId">
  182. <el-cascader v-model="regionValue" :options="areaOptions" placeholder="选择区域" style="width: 100%"
  183. @change="handleAreaChange" />
  184. </el-form-item>
  185. </el-col>
  186. <el-col :span="12">
  187. <el-form-item label="归属站点" prop="site">
  188. <el-select v-model="form.site" placeholder="选择站点" :disabled="!form.regionId">
  189. <el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" />
  190. </el-select>
  191. </el-form-item>
  192. </el-col>
  193. </el-row>
  194. <el-form-item label="详细地址">
  195. <el-row :gutter="10" style="margin-bottom: 10px">
  196. <el-col :span="24">
  197. <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
  198. style="width: 100%" />
  199. </el-col>
  200. </el-row>
  201. <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
  202. </el-form-item>
  203. <el-form-item>
  204. <el-button type="primary" style="width: 100%" @click="getGeolocation">获取经纬度</el-button>
  205. </el-form-item>
  206. <el-row :gutter="10">
  207. <el-col :span="12">
  208. <el-form-item label="经度" prop="longitude">
  209. <el-input v-model="form.longitude" placeholder="请获取/输入位置经度" />
  210. </el-form-item>
  211. </el-col>
  212. <el-col :span="12">
  213. <el-form-item label="纬度" prop="latitude">
  214. <el-input v-model="form.latitude" placeholder="请获取/输入位置纬度" />
  215. </el-form-item>
  216. </el-col>
  217. </el-row>
  218. </el-form>
  219. <template #footer>
  220. <div class="dialog-footer">
  221. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  222. <el-button @click="cancel">取 消</el-button>
  223. </div>
  224. </template>
  225. </el-dialog>
  226. <!-- 门店详情对话框 -->
  227. <el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="800px" append-to-body>
  228. <el-tabs v-model="activeTab" style="padding: 0 10px;">
  229. <el-tab-pane label="基础信息" name="basic">
  230. <el-descriptions :column="2" border>
  231. <el-descriptions-item label="门店名称">{{ detailData.name }}</el-descriptions-item>
  232. <el-descriptions-item label="商户分类">{{ detailData.tenantCatergoriesName || '-' }}</el-descriptions-item>
  233. <el-descriptions-item label="所属品牌">{{ detailData.tenantName || '-' }}</el-descriptions-item>
  234. <el-descriptions-item label="营业时间">{{ formatTime(detailData.startBusinessTime) }} - {{ formatTime(detailData.endBusinessTime) }}</el-descriptions-item>
  235. <el-descriptions-item label="有效期至">{{ parseTime(detailData.validity, '{y}-{m}-{d}') }}</el-descriptions-item>
  236. <el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
  237. <el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
  238. <el-descriptions-item label="所在区域">{{ detailData.regionName || '北京市朝阳区' }}</el-descriptions-item>
  239. <el-descriptions-item label="详细地址">{{ detailData.detailAddress }}</el-descriptions-item>
  240. <el-descriptions-item label="营业执照">
  241. <image-preview v-if="detailData.businessLicenseUrl" :src="detailData.businessLicenseUrl" :width="80" :height="60" />
  242. <span v-else>-</span>
  243. </el-descriptions-item>
  244. </el-descriptions>
  245. </el-tab-pane>
  246. <el-tab-pane label="服务订单记录" name="orders">
  247. <el-table :data="orderList" border style="width: 100%">
  248. <el-table-column label="订单号" prop="orderNo" min-width="150" />
  249. <el-table-column label="服务项目" prop="service" min-width="120" />
  250. <el-table-column label="客户" prop="customer" min-width="100" />
  251. <el-table-column label="金额" prop="amount" min-width="100" />
  252. <el-table-column label="下单时间" prop="time" min-width="160" />
  253. <el-table-column label="状态" align="center" width="100">
  254. <template #default="scope">
  255. <el-tag :type="scope.row.statusType" effect="plain" size="small">{{ scope.row.status }}</el-tag>
  256. </template>
  257. </el-table-column>
  258. </el-table>
  259. </el-tab-pane>
  260. </el-tabs>
  261. <template #footer>
  262. <div class="dialog-footer">
  263. <el-button @click="detailDialog.visible = false">关 闭</el-button>
  264. </div>
  265. </template>
  266. </el-dialog>
  267. </div>
  268. </template>
  269. <script setup name="Store" lang="ts">
  270. import { listStore, getStore, delStore, addStore, updateStore, listStoreStatus } from '@/api/system/store';
  271. import { StoreVO, StoreQuery, StoreForm, StoreStatusVO, SysStorePageBo } from '@/api/system/store/types';
  272. import { listOnStore } from '@/api/system/tenant';
  273. import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
  274. import { listOnStore as listServiceOnStore } from '@/api/service/list';
  275. import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
  276. import { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
  277. import { regionData, codeToText, textToCode } from 'element-china-area-data';
  278. import PageSelect from '@/components/PageSelect/index.vue';
  279. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  280. const storeList = ref<StoreVO[]>([]);
  281. const buttonLoading = ref(false);
  282. const loading = ref(true);
  283. const showSearch = ref(true);
  284. const ids = ref<Array<string | number>>([]);
  285. const single = ref(true);
  286. const multiple = ref(true);
  287. const total = ref(0);
  288. const queryFormRef = ref<ElFormInstance>();
  289. const storeFormRef = ref<ElFormInstance>();
  290. const brandSelectRef = ref<any>(null);
  291. const searchRegionValue = ref<any[]>([]); // 搜索的区域值
  292. const searchSiteOptions = ref<any[]>([]); // 搜索的站点选项
  293. /** 处理搜索区域选择变化 */
  294. const handleSearchAreaChange = (value: any[]) => {
  295. // 清空级联站点
  296. queryParams.value.station = undefined;
  297. if (value && value.length > 0) {
  298. const areaId = value[value.length - 1];
  299. queryParams.value.area = areaId;
  300. searchSiteOptions.value = areaStationList.value
  301. .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
  302. .map((item: any) => ({
  303. value: item.id,
  304. label: item.name
  305. }));
  306. } else {
  307. queryParams.value.area = undefined;
  308. searchSiteOptions.value = [];
  309. }
  310. handleQuery();
  311. };
  312. // 新增的响应式变量
  313. const regionValue = ref<any[]>([]);
  314. const province = ref('');
  315. const city = ref('');
  316. const district = ref('');
  317. const addressCascaderValue = ref<any[]>([]); // 省市区级联选择器值
  318. const brandList = ref<any[]>([]); // 品牌列表
  319. const brandLoading = ref(false); // 品牌加载状态
  320. const currentPage = ref(1); // 当前页码
  321. const brandKeyword = ref(''); // 搜索关键词
  322. const brandSelectVisible = ref(false); // 品牌选择框可见状态
  323. const brandTotal = ref(0); // 品牌总数
  324. const serviceList = ref<any[]>([]); // 服务项目列表
  325. const statusList = ref<StoreStatusVO[]>([]); // 状态列表
  326. const tenantCategoriesList = ref<any[]>([]); // 商户分类列表
  327. const tenantCategoriesTotal = ref(0); // 商户分类总数
  328. const areaStationList = ref<SysAreaStationOnStoreVo[]>([]); // 区域站点列表
  329. const areaOptions = ref<any[]>([]); // 所在区域树形选项
  330. const siteOptions = ref<any[]>([]); // 归属站点选项
  331. const dialog = reactive<DialogOption>({
  332. visible: false,
  333. title: ''
  334. });
  335. const detailDialog = reactive({
  336. visible: false,
  337. title: '门店详情'
  338. });
  339. const activeTab = ref('basic');
  340. const detailData = ref<any>({});
  341. const orderList = ref([
  342. { orderNo: 'ORD202402040001', service: '洗澡美容', customer: '张三', amount: '¥128', time: '2024-02-04 10:00', status: '已完成', statusType: 'success' },
  343. { orderNo: 'ORD202402040002', service: '寄养服务', customer: '李四', amount: '¥500', time: '2024-02-03 14:30', status: '已完成', statusType: 'success' },
  344. { orderNo: 'ORD202402030005', service: '疫苗注射', customer: '王五', amount: '¥80', time: '2024-02-01 09:00', status: '已取消', statusType: 'info' }
  345. ]);
  346. /** 详情按钮操作 */
  347. const handleDetail = async (row: StoreVO) => {
  348. const res = await getStore(row.id);
  349. // 合并列表里的关联数据,以便能够展示名称等额外字段
  350. detailData.value = { ...row, ...res.data };
  351. activeTab.value = 'basic';
  352. detailDialog.visible = true;
  353. };
  354. const initFormData: StoreForm = {
  355. id: undefined,
  356. logo: undefined,
  357. businessLicense: undefined,
  358. name: undefined,
  359. tenantCatergories: undefined,
  360. startBusinessTime: undefined,
  361. endBusinessTime: undefined,
  362. contact: undefined,
  363. contactNumber: undefined,
  364. validity: undefined,
  365. site: undefined,
  366. detailAddress: undefined,
  367. status: undefined,
  368. longitude: undefined,
  369. latitude: undefined,
  370. tenantId: undefined,
  371. services: [],
  372. regionId: undefined,
  373. }
  374. const data = reactive<PageData<StoreForm, SysStorePageBo>>({
  375. form: { ...initFormData },
  376. queryParams: {
  377. pageNum: 1,
  378. pageSize: 10,
  379. storeOrContact: undefined,
  380. area: undefined,
  381. station: undefined,
  382. status: undefined,
  383. params: {
  384. }
  385. },
  386. rules: {
  387. id: [
  388. { required: true, message: "序号不能为空", trigger: "blur" }
  389. ],
  390. businessLicense: [
  391. { required: true, message: "营业执照不能为空", trigger: "blur" }
  392. ],
  393. name: [
  394. { required: true, message: "门店名称不能为空", trigger: "blur" }
  395. ],
  396. tenantCatergories: [
  397. { required: true, message: "商户分类不能为空", trigger: "change" }
  398. ],
  399. startBusinessTime: [
  400. { required: true, message: "开始营业时间不能为空", trigger: "blur" }
  401. ],
  402. endBusinessTime: [
  403. { required: true, message: "结束营业时间不能为空", trigger: "blur" }
  404. ],
  405. contact: [
  406. { required: true, message: "联系人不能为空", trigger: "blur" }
  407. ],
  408. contactNumber: [
  409. { required: true, message: "联系电话不能为空", trigger: "blur" }
  410. ],
  411. validity: [
  412. { required: true, message: "有效期至不能为空", trigger: "blur" }
  413. ],
  414. tenantId: [
  415. { required: true, message: "租户编号不能为空", trigger: "change" }
  416. ],
  417. regionId: [
  418. { required: true, message: "所在区域不能为空", trigger: "change" }
  419. ],
  420. site: [
  421. { required: true, message: "归属站点不能为空", trigger: "change" }
  422. ],
  423. }
  424. });
  425. const { queryParams, form, rules } = toRefs(data);
  426. /** 查询门店管理列表 */
  427. const getList = async () => {
  428. loading.value = true;
  429. const res = await listStore(queryParams.value);
  430. storeList.value = res.rows;
  431. total.value = res.total;
  432. loading.value = false;
  433. }
  434. /** 取消按钮 */
  435. const cancel = () => {
  436. reset();
  437. dialog.visible = false;
  438. }
  439. /** 表单重置 */
  440. const reset = () => {
  441. form.value = { ...initFormData };
  442. // 重置新增的变量
  443. regionValue.value = [];
  444. province.value = '';
  445. city.value = '';
  446. district.value = '';
  447. addressCascaderValue.value = [];
  448. storeFormRef.value?.resetFields();
  449. }
  450. /** 搜索按钮操作 */
  451. const handleQuery = () => {
  452. queryParams.value.pageNum = 1;
  453. getList();
  454. }
  455. /** 重置按钮操作 */
  456. const resetQuery = () => {
  457. searchRegionValue.value = [];
  458. searchSiteOptions.value = [];
  459. queryParams.value.storeOrContact = undefined;
  460. queryParams.value.area = undefined;
  461. queryParams.value.station = undefined;
  462. queryParams.value.status = undefined;
  463. handleQuery();
  464. }
  465. /** 多选框选中数据 */
  466. const handleSelectionChange = (selection: StoreVO[]) => {
  467. ids.value = selection.map(item => item.id);
  468. single.value = selection.length != 1;
  469. multiple.value = !selection.length;
  470. }
  471. /** 新增按钮操作 */
  472. const handleAdd = () => {
  473. reset();
  474. dialog.visible = true;
  475. dialog.title = "添加门店管理";
  476. }
  477. /** 修改按钮操作 */
  478. const handleUpdate = async (row?: StoreVO) => {
  479. reset();
  480. const _id = row?.id || ids.value[0]
  481. const res = await getStore(_id);
  482. Object.assign(form.value, res.data);
  483. dialog.visible = true;
  484. dialog.title = "修改门店管理";
  485. }
  486. /** 提交按钮 */
  487. const submitForm = () => {
  488. storeFormRef.value?.validate(async (valid: boolean) => {
  489. if (valid) {
  490. buttonLoading.value = true;
  491. if (form.value.id) {
  492. await updateStore(form.value).finally(() => buttonLoading.value = false);
  493. } else {
  494. await addStore(form.value).finally(() => buttonLoading.value = false);
  495. }
  496. proxy?.$modal.msgSuccess("操作成功");
  497. dialog.visible = false;
  498. await getList();
  499. }
  500. });
  501. }
  502. /** 删除按钮操作 */
  503. const handleDelete = async (row?: StoreVO) => {
  504. const _ids = row?.id || ids.value;
  505. await proxy?.$modal.confirm('是否确认删除门店管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
  506. await delStore(_ids);
  507. proxy?.$modal.msgSuccess("删除成功");
  508. await getList();
  509. }
  510. /** 导出按钮操作 */
  511. const handleExport = () => {
  512. proxy?.download('system/store/export', {
  513. ...queryParams.value
  514. }, `store_${new Date().getTime()}.xlsx`)
  515. }
  516. /** 获取经纬度 */
  517. const getGeolocation = () => {
  518. if ('geolocation' in navigator) {
  519. navigator.geolocation.getCurrentPosition(
  520. (position) => {
  521. form.value.longitude = position.coords.longitude.toFixed(6);
  522. form.value.latitude = position.coords.latitude.toFixed(6);
  523. proxy?.$modal.msgSuccess('获取经纬度成功');
  524. },
  525. (error) => {
  526. let errorMessage = '获取位置失败';
  527. switch (error.code) {
  528. case error.PERMISSION_DENIED:
  529. errorMessage = '用户拒绝了地理定位请求';
  530. break;
  531. case error.POSITION_UNAVAILABLE:
  532. errorMessage = '位置信息不可用';
  533. break;
  534. case error.TIMEOUT:
  535. errorMessage = '获取位置超时';
  536. break;
  537. case error.UNKNOWN_ERROR:
  538. errorMessage = '未知错误';
  539. break;
  540. }
  541. proxy?.$modal.msgError(errorMessage);
  542. }
  543. );
  544. } else {
  545. proxy?.$modal.msgError('您的浏览器不支持地理定位');
  546. }
  547. };
  548. /** 获取品牌列表 */
  549. const getBrandList = async (pageNum = 1, keyword = '', append = false) => {
  550. brandLoading.value = true;
  551. // 确保参数格式正确,直接传递数字类型的pageNum
  552. const res = await listOnStore({ pageNum: pageNum, pageSize: 10 });
  553. if (res.code === 200) {
  554. if (append) {
  555. // 追加模式,用于分页加载
  556. brandList.value = [...brandList.value, ...res.rows];
  557. } else {
  558. // 替换模式,用于初始加载或搜索
  559. brandList.value = res.rows;
  560. }
  561. // 存储总数
  562. brandTotal.value = res.total || 0;
  563. console.log('总数', brandTotal.value);
  564. }
  565. brandLoading.value = false;
  566. };
  567. /** 获取服务项目列表 */
  568. const getServiceList = async () => {
  569. try {
  570. const res = await listServiceOnStore();
  571. // 转换数据格式,适配checkbox组件
  572. serviceList.value = res.data || res;
  573. } catch (error) {
  574. console.error('获取服务项目列表失败:', error);
  575. }
  576. };
  577. /** 获取区域站点列表 */
  578. const getAreaStationList = async () => {
  579. try {
  580. const res = await listAreaStationOnStore();
  581. const data = res.data || res;
  582. areaStationList.value = data;
  583. // 分离所在区域数据(type为0或1)
  584. const areaData = data.filter((item: any) => item.type === 0 || item.type === 1);
  585. // 构建树形结构
  586. areaOptions.value = buildTree(areaData, 0);
  587. // 初始化站点数据为空
  588. siteOptions.value = [];
  589. } catch (error) {
  590. console.error('获取区域站点列表失败:', error);
  591. }
  592. };
  593. /** 构建树形结构 */
  594. const buildTree = (data: any[], parentId: any): any[] => {
  595. return data
  596. .filter(item => String(item.parentId) === String(parentId))
  597. .map(item => ({
  598. value: item.id,
  599. label: item.name,
  600. children: buildTree(data, item.id)
  601. }));
  602. };
  603. /** 处理所在区域选择变化 */
  604. const handleAreaChange = (value: any[]) => {
  605. // 清空归属站点选择
  606. form.value.site = undefined;
  607. if (value && value.length > 0) {
  608. // 获取最后一级的id
  609. const areaId = value[value.length - 1];
  610. // 更新regionId
  611. form.value.regionId = areaId;
  612. // 过滤出parentId等于areaId的站点
  613. siteOptions.value = areaStationList.value
  614. .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
  615. .map((item: any) => ({
  616. value: item.id,
  617. label: item.name
  618. }));
  619. } else {
  620. // 如果没有选择区域,清空站点选项和regionId
  621. form.value.regionId = undefined;
  622. siteOptions.value = [];
  623. }
  624. };
  625. /** 获取商户分类列表 */
  626. const getTenantCategoriesList = async (pageNum = 1) => {
  627. try {
  628. const res = await listTenantCategoriesOnStore({ pageNum, pageSize: 10 });
  629. if (res.code === 200) {
  630. tenantCategoriesList.value = res.rows;
  631. tenantCategoriesTotal.value = res.total || 0;
  632. }
  633. } catch (error) {
  634. console.error('获取商户分类列表失败:', error);
  635. }
  636. };
  637. /** 处理品牌页面切换 */
  638. const handleBrandPageChange = (page: number) => {
  639. // 确保page是数字类型
  640. const pageNum = Number(page);
  641. currentPage.value = pageNum;
  642. getBrandList(pageNum, brandKeyword.value, false);
  643. };
  644. /** 处理商户分类分页 */
  645. const handleTenantCategoriesPageChange = (page: number) => {
  646. // 确保page是数字类型
  647. const pageNum = Number(page);
  648. getTenantCategoriesList(pageNum);
  649. };
  650. /** 处理商户分类选择框可见性变化 */
  651. const handleTenantCategoriesVisibleChange = (visible: boolean) => {
  652. if (visible) {
  653. getTenantCategoriesList(1);
  654. }
  655. };
  656. /** 远程搜索方法 */
  657. const remoteMethod = (query: string) => {
  658. brandKeyword.value = query;
  659. currentPage.value = 1;
  660. getBrandList(1, query, false);
  661. };
  662. /** 处理品牌选择框显示状态变化 */
  663. const handleBrandSelectVisibleChange = (visible: boolean) => {
  664. brandSelectVisible.value = visible;
  665. if (visible) {
  666. // 选择框显示时,重置页码并重新加载数据
  667. currentPage.value = 1;
  668. getBrandList(1, brandKeyword.value, false);
  669. }
  670. };
  671. // 监听省市区选择变化,自动追加到详细地址
  672. watch(
  673. addressCascaderValue,
  674. (newValue) => {
  675. if (newValue && newValue.length > 0) {
  676. // 将选中的省市区文本追加到详细地址
  677. const addressText = newValue.map(code => codeToText[code]).join('');
  678. if (form.value.detailAddress) {
  679. // 如果已有详细地址,将省市区放在前面
  680. form.value.detailAddress = addressText + form.value.detailAddress;
  681. } else {
  682. form.value.detailAddress = addressText;
  683. }
  684. }
  685. },
  686. { deep: true }
  687. );
  688. /** 获取服务项目名称 */
  689. const getServiceName = (serviceId: number): string => {
  690. const service = serviceList.value.find(item => item.id === serviceId);
  691. return service ? service.name : String(serviceId);
  692. };
  693. /** 获取状态列表 */
  694. const getStatusList = async () => {
  695. try {
  696. const res: any = await listStoreStatus();
  697. // 兼容可能的不同响应体结构
  698. statusList.value = res.data || res.rows || res;
  699. } catch (error) {
  700. console.error('获取状态列表失败:', error);
  701. }
  702. };
  703. /** 格式化时间为时分 */
  704. const formatTime = (time: string | number): string => {
  705. if (!time) return '';
  706. // 处理时间戳或日期字符串
  707. const date = new Date(time);
  708. // 检查是否是有效日期
  709. if (isNaN(date.getTime())) return '';
  710. // 格式化为 HH:mm
  711. const hours = date.getHours().toString().padStart(2, '0');
  712. const minutes = date.getMinutes().toString().padStart(2, '0');
  713. return `${hours}:${minutes}`;
  714. };
  715. onMounted(() => {
  716. getList();
  717. getBrandList();
  718. getServiceList();
  719. getAreaStationList();
  720. getStatusList();
  721. });
  722. </script>
  723. <style scoped>
  724. .brand-pagination {
  725. margin-top: 10px;
  726. padding-top: 10px;
  727. border-top: 1px solid #ebeef5;
  728. text-align: center;
  729. }
  730. .custom-pagination {
  731. display: flex;
  732. align-items: center;
  733. justify-content: center;
  734. gap: 10px;
  735. font-size: 14px;
  736. }
  737. .page-arrow {
  738. cursor: pointer;
  739. color: #606266;
  740. user-select: none;
  741. padding: 2px 8px;
  742. transition: color 0.3s;
  743. }
  744. .page-arrow:hover:not(.disabled) {
  745. color: #409eff;
  746. }
  747. .page-arrow.disabled {
  748. color: #c0c4cc;
  749. cursor: not-allowed;
  750. }
  751. .page-number {
  752. cursor: pointer;
  753. color: #606266;
  754. padding: 2px 8px;
  755. transition: all 0.3s;
  756. }
  757. .page-number:hover {
  758. color: #409eff;
  759. }
  760. .page-number.active {
  761. color: #409eff;
  762. font-weight: bold;
  763. }
  764. .total-text {
  765. margin-left: 15px;
  766. font-size: 12px;
  767. color: #909399;
  768. }
  769. </style>