index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. <template>
  2. <div class="page-container">
  3. <el-card shadow="never" class="table-card">
  4. <template #header>
  5. <div class="card-header">
  6. <span class="title">订单列表</span>
  7. <div class="right-panel">
  8. <el-radio-group v-model="filters.service" size="default" @change="handleSearch">
  9. <el-radio-button label="">全部类型</el-radio-button>
  10. <el-radio-button v-for="item in serviceOptions" :key="item.id" :label="item.id">{{ item.name }}</el-radio-button>
  11. </el-radio-group>
  12. <el-input
  13. v-model="filters.content"
  14. placeholder="订单号/商户/宠主/手机号"
  15. class="search-input"
  16. prefix-icon="Search"
  17. clearable
  18. @clear="handleSearch"
  19. @keyup.enter="handleSearch"
  20. />
  21. <el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
  22. </div>
  23. </div>
  24. <el-tabs v-model="filters.status" class="status-tabs" @tab-change="handleStatusTabChange">
  25. <el-tab-pane label="全部订单" name="" />
  26. <el-tab-pane label="待派单" name="0" />
  27. <el-tab-pane label="待接单" name="1" />
  28. <el-tab-pane label="服务中" name="2" />
  29. <el-tab-pane label="待商家确认" name="3" />
  30. <el-tab-pane label="已完成" name="4" />
  31. <el-tab-pane label="已取消" name="5" />
  32. </el-tabs>
  33. </template>
  34. <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ background: '#f5f7fa' }">
  35. <el-table-column prop="code" label="订单号" width="170" fixed="left" />
  36. <el-table-column label="服务类型" width="190">
  37. <template #default="{ row }">
  38. <div class="service-type-cell">
  39. <el-tag>{{ getServiceName(row.service) }}</el-tag>
  40. <el-tag v-if="getServiceModeTag(row)" class="sub-tag" type="warning" effect="plain">{{ getServiceModeTag(row) }}</el-tag>
  41. <el-tag v-if="getServiceOrderTypeTag(row)" class="sub-tag" :type="getServiceOrderTypeTag(row).type" effect="dark">{{
  42. getServiceOrderTypeTag(row).label
  43. }}</el-tag>
  44. </div>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="宠物信息" min-width="150">
  48. <template #default="{ row }">
  49. <div class="pet-info">
  50. <el-avatar :size="30" class="avatar-type">{{ row.petName?.charAt(0) }}</el-avatar>
  51. <div class="pet-detail">
  52. <div class="pet-name">
  53. {{ row.petName }}
  54. </div>
  55. <div class="pet-breed">{{ row.petBreed }}</div>
  56. </div>
  57. </div>
  58. </template>
  59. </el-table-column>
  60. <el-table-column label="所属用户" width="120" prop="customerName">
  61. <template #default="{ row }">
  62. <span style="font-weight: 500">{{ row.customerName }}</span>
  63. </template>
  64. </el-table-column>
  65. <el-table-column label="城市/区域" width="140">
  66. <template #default="{ row }">
  67. <div>{{ getCityDistrictText(row).city || '-' }}</div>
  68. <div class="sub-text">{{ getCityDistrictText(row).district || '-' }}</div>
  69. </template>
  70. </el-table-column>
  71. <el-table-column label="商户/下单人" min-width="160">
  72. <template #default="{ row }">
  73. <div class="merchant-info">
  74. <div>{{ row.storeName }}</div>
  75. <div class="sub-text" v-if="row.placerUsername">{{ row.placerUsername }}</div>
  76. </div>
  77. </template>
  78. </el-table-column>
  79. <el-table-column prop="createTime" label="下单时间" width="165" sortable>
  80. <template #default="{ row }">
  81. <span class="time-text">{{ row.createTime }}</span>
  82. </template>
  83. </el-table-column>
  84. <el-table-column prop="createTime" label="预约服务时间" width="165" sortable>
  85. <template #default="{ row }">
  86. <span class="time-text">{{ row.serviceTime }}</span>
  87. </template>
  88. </el-table-column>
  89. <el-table-column label="订单状态" width="100">
  90. <template #default="{ row }">
  91. <div class="status-cell">
  92. <div class="status-dot" :class="getStatusClass(row.status)"></div>
  93. <span>{{ getStatusName(row.status) }}</span>
  94. </div>
  95. </template>
  96. </el-table-column>
  97. <el-table-column label="履约信息" width="140">
  98. <template #default="{ row }">
  99. <div v-if="row.fulfillerName" class="fulfiller-info">
  100. <span class="fulfiller-name">{{ row.fulfillerName }}</span>
  101. <span class="fulfiller-fee" v-if="row.price !== null && row.price !== undefined">¥{{ row.price }}</span>
  102. </div>
  103. <span v-else class="text-gray">暂未指派</span>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="操作" width="200" fixed="right">
  107. <template #default="{ row }">
  108. <div class="op-cell">
  109. <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
  110. <el-button v-if="row.status === 0" link type="success" size="small" @click="openDispatchDialog(row)">派单</el-button>
  111. <el-button v-if="[1, 2].includes(row.status)" link type="warning" size="small" @click="openDispatchDialog(row)">重新派单</el-button>
  112. <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small" @click="handleCancel(row)">取消</el-button>
  113. <el-dropdown v-if="[2, 3, 4].includes(row.status)" trigger="click" @command="(cmd) => handleCommand(cmd, row)">
  114. <span class="el-dropdown-link">
  115. 更多<el-icon class="el-icon--right">
  116. <ArrowDown />
  117. </el-icon>
  118. </span>
  119. <template #dropdown>
  120. <el-dropdown-menu>
  121. <el-dropdown-item v-if="row.status === 3" command="complete">确认完成</el-dropdown-item>
  122. <el-dropdown-item v-if="[3, 4].includes(row.status)" command="care_summary">护理小结</el-dropdown-item>
  123. <el-dropdown-item command="reward">奖惩</el-dropdown-item>
  124. <el-dropdown-item command="remark">备注</el-dropdown-item>
  125. </el-dropdown-menu>
  126. </template>
  127. </el-dropdown>
  128. </div>
  129. </template>
  130. </el-table-column>
  131. </el-table>
  132. <div class="pagination-container">
  133. <el-pagination
  134. v-model:current-page="pagination.current"
  135. v-model:page-size="pagination.size"
  136. :page-sizes="[10, 20, 50, 100]"
  137. layout="total, sizes, prev, pager, next, jumper"
  138. :total="pagination.total"
  139. @size-change="handleSizeChange"
  140. @current-change="handleCurrentChange"
  141. />
  142. </div>
  143. </el-card>
  144. <!-- 组件 -->
  145. <OrderDetailDrawer
  146. v-model:visible="detailVisible"
  147. :order="currentOrder"
  148. @dispatch="openDispatchDialog"
  149. @cancel="handleCancel"
  150. @command="handleCommand"
  151. @care-summary="openCareSummary"
  152. />
  153. <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder" @submit="handleDispatchSubmit" />
  154. <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
  155. <RewardDialog v-model:visible="rewardDialogVisible" :order="currentOperateRow" @submit="handleRewardSubmit" />
  156. <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
  157. </div>
  158. </template>
  159. <script setup>
  160. import { ref, reactive, onMounted, nextTick } from 'vue';
  161. import { ElMessage, ElMessageBox } from 'element-plus';
  162. import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
  163. import DispatchDialog from './components/DispatchDialog.vue';
  164. import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
  165. import RewardDialog from './components/RewardDialog.vue';
  166. import RemarkDialog from './components/RemarkDialog.vue';
  167. import { listOnStore as listServiceOnStore } from '@/api/service/list/index';
  168. import { listSubOrder } from '@/api/order/subOrder/index';
  169. import { dispatchSubOrder } from '@/api/order/subOrder/index';
  170. import { getSubOrderInfo } from '@/api/order/subOrder/index';
  171. import { cancelSubOrder } from '@/api/order/subOrder/index';
  172. import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
  173. import { getStore } from '@/api/system/store';
  174. const loading = ref(false);
  175. const filters = reactive({
  176. service: '',
  177. status: '',
  178. content: ''
  179. });
  180. const pagination = reactive({
  181. current: 1,
  182. size: 10,
  183. total: 100
  184. });
  185. const tableData = ref([]);
  186. const serviceOptions = ref([]);
  187. const areaStationList = ref([]);
  188. const areaStationMap = ref({});
  189. const storeMap = ref({});
  190. onMounted(() => {
  191. getServiceList();
  192. getAreaStationList();
  193. handleSearch();
  194. });
  195. const getServiceList = () => {
  196. listServiceOnStore().then((res) => {
  197. serviceOptions.value = res.data || [];
  198. });
  199. };
  200. const getAreaStationList = () => {
  201. listAreaStationOnStore().then((res) => {
  202. const list = res.data || [];
  203. areaStationList.value = list;
  204. const map = {};
  205. for (const item of list) {
  206. if (item && item.id !== undefined && item.id !== null) map[item.id] = item;
  207. }
  208. areaStationMap.value = map;
  209. });
  210. };
  211. const getCityDistrictText = (row) => {
  212. if (!row) return { city: '', district: '' };
  213. const map = areaStationMap.value || {};
  214. let stationId = row.site;
  215. if (!map[stationId] && row.store) {
  216. const store = (storeMap.value || {})[row.store];
  217. if (store?.site) stationId = store.site;
  218. }
  219. if (!stationId) return { city: '', district: '' };
  220. const station = map[stationId];
  221. if (!station) return { city: '', district: '' };
  222. const parent = station.parentId ? map[station.parentId] : undefined;
  223. if (!parent) return { city: station.name || '', district: '' };
  224. if (parent.type === 0) return { city: parent.name || '', district: '' };
  225. if (parent.type === 1) {
  226. const city = parent.parentId ? map[parent.parentId] : undefined;
  227. return { city: city?.name || '', district: parent.name || '' };
  228. }
  229. return { city: '', district: parent.name || '' };
  230. };
  231. const handleStatusTabChange = async () => {
  232. pagination.current = 1;
  233. await nextTick();
  234. handleSearch();
  235. };
  236. const handleSearch = () => {
  237. loading.value = true;
  238. listSubOrder({
  239. pageNum: pagination.current,
  240. pageSize: pagination.size,
  241. service: filters.service !== '' ? filters.service : undefined,
  242. status: filters.status !== '' ? Number(filters.status) : undefined,
  243. content: filters.content || undefined
  244. })
  245. .then((res) => {
  246. tableData.value = res.rows || [];
  247. pagination.total = res.total || 0;
  248. loadStoresForRows(tableData.value);
  249. loading.value = false;
  250. })
  251. .catch(() => {
  252. loading.value = false;
  253. });
  254. };
  255. const loadStoresForRows = async (rows) => {
  256. const map = storeMap.value || {};
  257. const ids = Array.from(new Set((rows || []).map((r) => r?.store).filter(Boolean)));
  258. const missing = ids.filter((id) => map[id] === undefined);
  259. if (missing.length === 0) return;
  260. await Promise.all(
  261. missing.map(async (id) => {
  262. try {
  263. const res = await getStore(id);
  264. if (res?.data) map[id] = res.data;
  265. } catch {
  266. map[id] = null;
  267. }
  268. })
  269. );
  270. storeMap.value = { ...map };
  271. };
  272. const handleSizeChange = (val) => {
  273. pagination.size = val;
  274. handleSearch();
  275. };
  276. const handleCurrentChange = (val) => {
  277. pagination.current = val;
  278. handleSearch();
  279. };
  280. const getServiceName = (serviceId) => {
  281. const item = serviceOptions.value.find((i) => i.id === serviceId);
  282. return item ? item.name : '未知服务';
  283. };
  284. const getServiceModeTag = (row) => {
  285. const t = row?.type;
  286. if (t === 0 || t === '0' || t === 1 || t === '1') return '往返';
  287. return '';
  288. };
  289. const getServiceOrderTypeTag = (row) => {
  290. const t = row?.type;
  291. if (t === 0 || t === '0') return { label: '接', type: 'primary' };
  292. if (t === 1 || t === '1') return { label: '送', type: 'success' };
  293. if (t === 2 || t === '2') return { label: '单程接', type: 'primary' };
  294. if (t === 3 || t === '3') return { label: '单程送', type: 'success' };
  295. return null;
  296. };
  297. const getStatusName = (status) => {
  298. const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' };
  299. return map[status] || '未知';
  300. };
  301. const getStatusClass = (status) => {
  302. const map = { 0: 'pending_dispatch', 1: 'pending_accept', 2: 'serving', 3: 'pending_confirm', 4: 'completed', 5: 'cancelled' };
  303. return map[status] || 'pending_dispatch';
  304. };
  305. const getFulfillerStatusText = (status) => {
  306. const statusMap = {
  307. resting: '休息',
  308. busy: '接单中',
  309. disabled: '禁用'
  310. };
  311. return statusMap[status] || status;
  312. };
  313. const getFulfillerStatusType = (status) => {
  314. const typeMap = {
  315. resting: 'info',
  316. busy: 'success',
  317. disabled: 'danger'
  318. };
  319. return typeMap[status] || 'info';
  320. };
  321. // 弹窗状态管理
  322. const detailVisible = ref(false);
  323. const currentOrder = ref(null);
  324. const dispatchDialogVisible = ref(false);
  325. const currentDispatchOrder = ref(null);
  326. const careSummaryVisible = ref(false);
  327. const careSummaryOrder = ref(null);
  328. const rewardDialogVisible = ref(false);
  329. const remarkDialogVisible = ref(false);
  330. const currentOperateRow = ref(null);
  331. // 详情
  332. const handleDetail = async (row) => {
  333. const typeName = getServiceName(row?.service);
  334. const isTransport = row?.mode === 1 || row?.mode === '1';
  335. const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
  336. currentOrder.value = {
  337. ...row,
  338. orderNo: row?.code || row?.orderCode || row?.orderNo || row?.orderNumber || row?.no || '',
  339. type: row?.typeCode || row?.type || typeCode,
  340. serviceItem: getServiceName(row?.service) || row?.serviceName || row?.service || '',
  341. userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
  342. address: '某小区5号楼2单元101',
  343. groupBuyPackage: '',
  344. transportType: row.splitType || row.transportType,
  345. detail: {
  346. ...row.detail,
  347. pickTime: '2024-02-05 09:30',
  348. pickAddr: row.detail?.pickAddr || '北京市朝阳区某小区5号楼2单元101',
  349. pickContact: '李先生',
  350. pickPhone: '13812345678',
  351. dropTime: '2024-02-05 18:30',
  352. dropAddr: row.detail?.dropAddr || '北京市朝阳区某小区5号楼2单元101',
  353. dropContact: '李先生',
  354. dropPhone: '13812345678',
  355. packageName: row.detail?.packageName || '精细洗护套餐A',
  356. petStatus: '胆小,需安抚',
  357. area: '北京市朝阳区某小区5号楼2单元101'
  358. },
  359. petGender: 'male',
  360. petAge: '2岁',
  361. petWeight: '15kg',
  362. petVaccine: '已接种',
  363. petSterilized: true,
  364. petCharacter: '活泼好动,喜欢球类玩具',
  365. petHealth: '健康良好',
  366. fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
  367. fulfillerPhone: '13812345678',
  368. fulfillerStation: '朝阳服务站',
  369. orderLogs: [
  370. { time: '2024-02-04 09:30', title: '订单创建', content: '商户提交订单', icon: 'Document' },
  371. { time: '2024-02-04 10:00', title: '系统派单', content: '指派给 王大力', icon: 'Bicycle' },
  372. { time: '2024-02-04 10:05', title: '接单成功', content: '履约者已确认接单', icon: 'CircleCheck' },
  373. { time: '2024-02-04 13:55', title: '到达服务点', content: '履约者已打卡', icon: 'Location' }
  374. ]
  375. };
  376. try {
  377. const res = await getSubOrderInfo(row?.id);
  378. const info = res?.data?.data || res?.data;
  379. if (info) {
  380. currentOrder.value = {
  381. ...(currentOrder.value || {}),
  382. id: info.id,
  383. orderNo: info.code || currentOrder.value?.orderNo,
  384. code: info.code,
  385. subOrderType: info.type,
  386. status: info.status ?? currentOrder.value?.status,
  387. mode: info.mode ?? currentOrder.value?.mode,
  388. type: info.mode === 1 || info.mode === '1' ? 'transport' : currentOrder.value?.type,
  389. transportType: info.mode === 1 || info.mode === '1' ? 'round' : currentOrder.value?.transportType,
  390. serviceTime: info.serviceTime || currentOrder.value?.serviceTime,
  391. endServiceTime: info.endServiceTime || currentOrder.value?.endServiceTime,
  392. contact: info.contact || currentOrder.value?.contact,
  393. contactPhoneNumber: info.contactPhoneNumber || currentOrder.value?.contactPhoneNumber,
  394. toAddress: info.toAddress || currentOrder.value?.toAddress,
  395. address:
  396. info.mode === 1 || info.mode === '1'
  397. ? info.toAddress || currentOrder.value?.address
  398. : info.address || info.toAddress || currentOrder.value?.address,
  399. price: info.price !== undefined && info.price !== null ? Number(info.price) / 100 : currentOrder.value?.price,
  400. fulfillerFee: info.price !== undefined && info.price !== null ? Number(info.price) / 100 : currentOrder.value?.fulfillerFee,
  401. merchantName: info.storeName || currentOrder.value?.merchantName,
  402. platformId: info.platformId ?? currentOrder.value?.platformId,
  403. groupBuyPackage: info.groupPurchasePackageName || currentOrder.value?.groupBuyPackage,
  404. fulfiller: info.fulfiller ?? currentOrder.value?.fulfiller,
  405. detail: {
  406. ...(currentOrder.value?.detail || {}),
  407. pickTime: info.serviceTime || currentOrder.value?.detail?.pickTime,
  408. pickAddr: info.fromAddress || currentOrder.value?.detail?.pickAddr,
  409. pickContact: info.contact || currentOrder.value?.detail?.pickContact,
  410. pickPhone: info.contactPhoneNumber || currentOrder.value?.detail?.pickPhone,
  411. dropTime: info.endServiceTime || currentOrder.value?.detail?.dropTime,
  412. dropAddr: info.toAddress || currentOrder.value?.detail?.dropAddr,
  413. dropContact: info.contact || currentOrder.value?.detail?.dropContact,
  414. dropPhone: info.contactPhoneNumber || currentOrder.value?.detail?.dropPhone,
  415. fromAddress: info.fromAddress || currentOrder.value?.detail?.fromAddress,
  416. toAddress: info.toAddress || currentOrder.value?.detail?.toAddress,
  417. area: info.area || info.address || info.toAddress || currentOrder.value?.detail?.area,
  418. packageName: info.packageName || info.servicePackageName || info.package || currentOrder.value?.detail?.packageName,
  419. petStatus: info.petStatus || info.specialRequirement || info.requirement || currentOrder.value?.detail?.petStatus,
  420. fromCode: info.fromCode || currentOrder.value?.detail?.fromCode,
  421. toCode: info.toCode || currentOrder.value?.detail?.toCode
  422. }
  423. };
  424. }
  425. } catch {}
  426. detailVisible.value = true;
  427. };
  428. // 取消订单
  429. const handleCancel = (row) => {
  430. ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' }).then(() => {
  431. cancelSubOrder({ orderId: row?.id }).then(() => {
  432. ElMessage.success('订单已取消');
  433. handleSearch();
  434. });
  435. });
  436. };
  437. // 派单
  438. const openDispatchDialog = (row) => {
  439. const typeName = getServiceName(row?.service);
  440. const isTransport = row?.mode === 1 || row?.mode === '1';
  441. const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
  442. const t = row?.subOrderType ?? row?.type;
  443. const transportType =
  444. t === 0 || t === '0' || t === 1 || t === '1'
  445. ? 'round'
  446. : t === 2 || t === '2'
  447. ? 'pick'
  448. : t === 3 || t === '3'
  449. ? 'drop'
  450. : row?.splitType || row?.transportType;
  451. const toAddress = row?.toAddress || '';
  452. const pickAddr = isTransport ? toAddress : '';
  453. const dropAddr = isTransport ? toAddress : '';
  454. const address = isTransport ? '' : toAddress;
  455. const orderObj = {
  456. id: row.id,
  457. typeCode,
  458. transportType,
  459. time: row.serviceTime || row.appointTime || row.createTime,
  460. status: row.status,
  461. address,
  462. pickAddr,
  463. dropAddr,
  464. riderId: row.riderId || row.fulfiller || null
  465. };
  466. currentDispatchOrder.value = orderObj;
  467. dispatchDialogVisible.value = true;
  468. };
  469. const handleDispatchSubmit = (payload) => {
  470. if (!currentDispatchOrder.value) return;
  471. const priceFen = Math.round(Number(payload.fee || 0) * 100);
  472. dispatchSubOrder({
  473. orderId: currentDispatchOrder.value.id,
  474. fulfiller: payload.riderId,
  475. price: priceFen
  476. }).then(() => {
  477. ElMessage.success('派单成功');
  478. const row = tableData.value.find((r) => r.id === currentDispatchOrder.value.id);
  479. if (row) {
  480. row.status = 1;
  481. row.fulfillerName = payload.riderName || 'Unknown';
  482. row.price = payload.fee;
  483. }
  484. handleSearch();
  485. });
  486. };
  487. // 护理小结
  488. const openCareSummary = (row) => {
  489. careSummaryOrder.value = {
  490. ...row,
  491. petAge: '3岁',
  492. petGender: 'male',
  493. petTags: ['易过敏', '胆小'],
  494. petWeight: '30 kg',
  495. petPersonality: '活泼,超级粘人,喜欢玩球',
  496. homeTime: '2023-01-01',
  497. houseType: '电梯',
  498. entryMethod: '密码开门',
  499. entryDetail: '密码: 123456 (仅限服务期间使用)',
  500. healthStatus: '健康',
  501. vaccineImg: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
  502. allergy: '海鲜'
  503. };
  504. careSummaryVisible.value = true;
  505. };
  506. const saveCareSummary = (text) => {
  507. if (careSummaryOrder.value) {
  508. careSummaryOrder.value.careSummary = text;
  509. ElMessage.success('护理小结已保存');
  510. handleSearch();
  511. }
  512. };
  513. // 奖惩
  514. const openRewardDialog = (row) => {
  515. currentOperateRow.value = row;
  516. rewardDialogVisible.value = true;
  517. };
  518. const handleRewardSubmit = (form) => {
  519. ElMessage.success(`操作成功:${form.type === 'reward' ? '奖励' : '惩罚'}已执行`);
  520. };
  521. // 备注
  522. const openRemarkDialog = (row) => {
  523. currentOperateRow.value = row;
  524. remarkDialogVisible.value = true;
  525. };
  526. const handleRemarkSubmit = (text) => {
  527. if (currentOperateRow.value) {
  528. currentOperateRow.value.remark = text;
  529. ElMessage.success('备注已更新');
  530. }
  531. };
  532. // 更多操作
  533. const handleCommand = (cmd, row) => {
  534. if (cmd === 'reward') openRewardDialog(row);
  535. if (cmd === 'remark') openRemarkDialog(row);
  536. if (cmd === 'care_summary') openCareSummary(row);
  537. if (cmd === 'complete') {
  538. ElMessageBox.confirm('确认将该订单手动标记为完成吗?', '提示', { type: 'warning' }).then(() => {
  539. row.status = 4;
  540. ElMessage.success('订单已标记完成');
  541. });
  542. }
  543. if (cmd === 'delete') {
  544. ElMessageBox.confirm('确认删除该订单吗?此操作不可恢复', '警告', { type: 'error' }).then(() => {
  545. tableData.value = tableData.value.filter((item) => item.id !== row.id);
  546. ElMessage.success('订单已删除');
  547. });
  548. }
  549. };
  550. </script>
  551. <style scoped>
  552. .page-container {
  553. padding: 20px;
  554. }
  555. .card-header {
  556. display: flex;
  557. justify-content: space-between;
  558. align-items: center;
  559. }
  560. .title {
  561. font-weight: bold;
  562. font-size: 18px;
  563. }
  564. .right-panel {
  565. display: flex;
  566. gap: 10px;
  567. align-items: center;
  568. }
  569. .search-input {
  570. width: 220px;
  571. }
  572. .status-tabs {
  573. margin-top: 10px;
  574. margin-bottom: -10px;
  575. }
  576. .pagination-container {
  577. display: flex;
  578. justify-content: flex-end;
  579. margin-top: 20px;
  580. }
  581. /* Table Content Styles */
  582. .service-type-cell {
  583. display: flex;
  584. flex-direction: row;
  585. gap: 4px;
  586. align-items: center;
  587. }
  588. .sub-tag {
  589. font-size: 11px;
  590. height: 20px;
  591. padding: 0 5px;
  592. }
  593. .pet-info {
  594. display: flex;
  595. align-items: center;
  596. gap: 10px;
  597. }
  598. .pet-info .el-avatar {
  599. background: #e0eaff;
  600. color: #409eff;
  601. font-weight: bold;
  602. flex-shrink: 0;
  603. }
  604. .pet-info .avatar-feeding {
  605. background: #fdf6ec;
  606. color: #e6a23c;
  607. }
  608. .pet-info .avatar-washing {
  609. background: #f0f9eb;
  610. color: #67c23a;
  611. }
  612. .pet-detail {
  613. display: flex;
  614. flex-direction: column;
  615. line-height: 1.4;
  616. }
  617. .pet-name {
  618. font-weight: bold;
  619. font-size: 14px;
  620. color: #303133;
  621. }
  622. .pet-breed {
  623. color: #909399;
  624. font-weight: normal;
  625. font-size: 12px;
  626. }
  627. .merchant-info {
  628. display: flex;
  629. flex-direction: column;
  630. line-height: 1.4;
  631. }
  632. .sub-text {
  633. font-size: 12px;
  634. color: #999;
  635. }
  636. .text-gray {
  637. color: #ccc;
  638. font-style: italic;
  639. }
  640. .time-text {
  641. font-size: 13px;
  642. color: #606266;
  643. }
  644. .status-cell {
  645. display: flex;
  646. align-items: center;
  647. }
  648. .status-dot {
  649. width: 6px;
  650. height: 6px;
  651. border-radius: 50%;
  652. margin-right: 6px;
  653. background-color: #909399;
  654. }
  655. .status-dot.pending_dispatch {
  656. background-color: #f56c6c;
  657. box-shadow: 0 0 4px rgba(245, 108, 108, 0.4);
  658. }
  659. .status-dot.pending_accept {
  660. background-color: #e6a23c;
  661. }
  662. .status-dot.serving {
  663. background-color: #409eff;
  664. }
  665. .status-dot.pending_confirm {
  666. background-color: #bf24e8;
  667. }
  668. .status-dot.completed {
  669. background-color: #67c23a;
  670. }
  671. .status-dot.cancelled {
  672. background-color: #909399;
  673. }
  674. .fulfiller-info {
  675. display: flex;
  676. flex-direction: column;
  677. }
  678. .fulfiller-name {
  679. font-weight: 500;
  680. color: #333;
  681. }
  682. .fulfiller-fee {
  683. font-size: 12px;
  684. color: #e6a23c;
  685. }
  686. .op-cell {
  687. display: flex;
  688. align-items: center;
  689. gap: 8px;
  690. }
  691. .el-dropdown-link {
  692. cursor: pointer;
  693. color: #409eff;
  694. font-size: 12px;
  695. display: flex;
  696. align-items: center;
  697. line-height: 1;
  698. height: 24px;
  699. }
  700. </style>