list.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. <template>
  2. <view class="page">
  3. <u-navbar :placeholder="true" :border-bottom="false" :custom-back="goBack" title="菜品列表">
  4. <view slot="right" class="nav-right">
  5. <template v-if="!batchMode">
  6. <image class="nav-icon" src="/static/merchantDish/search.png" mode="aspectFit" @click="showSearch = !showSearch"></image>
  7. <image class="nav-icon" src="/static/merchantDish/batch.png" mode="aspectFit" @click="enterBatch"></image>
  8. </template>
  9. <text v-else class="cancel-btn" @click="exitBatch">取消</text>
  10. </view>
  11. </u-navbar>
  12. <view class="search-bar" v-if="showSearch && !batchMode">
  13. <u-search
  14. v-model="keyword"
  15. placeholder="请输入菜品/套餐名称"
  16. :show-action="true"
  17. action-text="搜索"
  18. @search="onSearch"
  19. @custom="onSearch"
  20. ></u-search>
  21. </view>
  22. <view class="filter-wrap">
  23. <view class="filters">
  24. <view :class="['filter-item', pickerShow && pickerType === 'status' ? 'active' : '']" @click="openPicker('status')">
  25. <text class="filter-text">{{ statusLabel }}</text>
  26. <image
  27. :class="['filter-arrow', pickerShow && pickerType === 'status' ? 'up' : '']"
  28. src="/static/merchantDish/selete.png"
  29. mode="aspectFit"
  30. ></image>
  31. </view>
  32. <view :class="['filter-item', pickerShow && pickerType === 'type' ? 'active' : '']" @click="openPicker('type')">
  33. <text class="filter-text">{{ typeLabel }}</text>
  34. <image
  35. :class="['filter-arrow', pickerShow && pickerType === 'type' ? 'up' : '']"
  36. src="/static/merchantDish/selete.png"
  37. mode="aspectFit"
  38. ></image>
  39. </view>
  40. <view :class="['filter-item', pickerShow && pickerType === 'category' ? 'active' : '']" @click="openPicker('category')">
  41. <text class="filter-text">{{ categoryLabel }}</text>
  42. <image
  43. :class="['filter-arrow', pickerShow && pickerType === 'category' ? 'up' : '']"
  44. src="/static/merchantDish/selete.png"
  45. mode="aspectFit"
  46. ></image>
  47. </view>
  48. </view>
  49. <!-- 筛选下拉:状态 / 菜品类型 / 品类 -->
  50. <view class="filter-dropdown" v-if="pickerShow">
  51. <view class="filter-panel">
  52. <view
  53. v-for="(opt, idx) in pickerList"
  54. :key="idx"
  55. :class="['filter-option', isOptionActive(opt) ? 'on' : '']"
  56. @click="onPick(opt)"
  57. >
  58. {{ opt.text }}
  59. </view>
  60. </view>
  61. <view class="filter-mask" @click="closePicker"></view>
  62. </view>
  63. </view>
  64. <!-- 团餐报名提示条 -->
  65. <view class="banner" v-if="bannerVisible" @click="onBannerClick">
  66. <rich-text :nodes="bannerNodes"></rich-text>
  67. </view>
  68. <view class="list-wrap" :style="{ paddingBottom: batchMode ? '140rpx' : '160rpx' }">
  69. <ListScrollView
  70. ref="listScroll"
  71. :api="api"
  72. :init="initParams"
  73. :pageSize="20"
  74. emptyText="暂无菜品"
  75. @afterFetch="afterFetch"
  76. >
  77. <template v-slot:list="{ data }">
  78. <view class="list">
  79. <view class="card" v-for="item in data" :key="item.id" @click="toggleSelect(item)">
  80. <image
  81. class="belong-tag"
  82. :src="item.belong === 'group' ? '/static/merchantDish/groupmeal.png' : '/static/merchantDish/store.png'"
  83. mode="heightFix"
  84. ></image>
  85. <view class="check" v-if="batchMode">
  86. <view :class="['dot', selectedIds.includes(item.id) ? 'on' : '']">
  87. <u-icon v-if="selectedIds.includes(item.id)" name="checkmark" color="#fff" size="20"></u-icon>
  88. </view>
  89. </view>
  90. <view class="thumb-wrap">
  91. <image class="thumb" :src="item.image" mode="aspectFill"></image>
  92. </view>
  93. <view class="info">
  94. <view class="row-top">
  95. <text class="name">{{ item.name }}</text>
  96. <text :class="['status', item.status]">{{ statusText[item.status] || item.statusName }}</text>
  97. </view>
  98. <view class="desc">{{ item.desc }}</view>
  99. <view class="meta">
  100. 每日库存: {{ item.stock != null ? item.stock : 0 }}
  101. <text class="meta-gap">合计销量: {{ formatSales(item.sales) }}</text>
  102. </view>
  103. <view class="price-line">
  104. <text class="price">¥{{ formatPrice(item.price) }}</text>
  105. <text class="origin" v-if="item.originPrice != null && item.originPrice !== ''">{{ formatPrice(item.originPrice) }}</text>
  106. </view>
  107. <view class="actions" v-if="!batchMode" @click.stop>
  108. <template v-if="item.status === 'selling' || item.status === 'soldout'">
  109. <view class="btn primary" @click="onOffShelf(item)">下架</view>
  110. </template>
  111. <template v-else-if="item.status === 'pending'">
  112. <text class="btn-link" @click="onDelete(item)">删除</text>
  113. <text class="btn-link" @click="goEdit(item)">编辑</text>
  114. <view class="btn primary" @click="onOnShelf(item)">上架</view>
  115. </template>
  116. <template v-else-if="item.status === 'rejected'">
  117. <text class="btn-link" @click="onDelete(item)">删除</text>
  118. <text class="btn-link" @click="goEdit(item)">编辑</text>
  119. </template>
  120. </view>
  121. </view>
  122. </view>
  123. </view>
  124. </template>
  125. </ListScrollView>
  126. </view>
  127. <!-- 底部:新建 / 批量操作 -->
  128. <view class="footer" v-if="!batchMode">
  129. <view class="primary-btn" @click="goCreate">新建菜品</view>
  130. </view>
  131. <view class="batch-footer" v-else>
  132. <view class="all" @click="toggleAll">
  133. <view :class="['dot', isAllSelected ? 'on' : '']">
  134. <u-icon v-if="isAllSelected" name="checkmark" color="#fff" size="20"></u-icon>
  135. </view>
  136. 全选
  137. </view>
  138. <view class="batch-btns">
  139. <template v-if="filterStatus === '' || filterStatus === 'reviewing'">
  140. <view class="b-btn disabled">批量删除</view>
  141. <view class="b-btn disabled">批量下架</view>
  142. <view class="b-btn disabled">批量上架</view>
  143. </template>
  144. <template v-else-if="filterStatus === 'selling' || filterStatus === 'soldout'">
  145. <view class="b-btn danger" @click="batchOff">批量下架</view>
  146. </template>
  147. <template v-else-if="filterStatus === 'pending'">
  148. <view class="b-btn outline" @click="batchDelete">批量删除</view>
  149. <view class="b-btn danger" @click="batchOn">批量上架</view>
  150. </template>
  151. <template v-else-if="filterStatus === 'rejected'">
  152. <view class="b-btn danger" @click="batchDelete">批量删除</view>
  153. </template>
  154. </view>
  155. </view>
  156. <!-- 团餐下架确认 -->
  157. <view class="mask" v-if="offShelfShow" @click="closeOffShelf">
  158. <view class="confirm-dialog" @click.stop>
  159. <view class="confirm-title">提示</view>
  160. <view class="confirm-content">
  161. 该菜品当前有未成团订单{{ (offShelfItem && offShelfItem.ungroupedOrderCount) || 0 }}份,已成团{{ (offShelfItem && offShelfItem.groupedOrderCount) || 0 }}份,是否要确定下架
  162. </view>
  163. <view class="confirm-footer">
  164. <view class="confirm-btn" @click="closeOffShelf">取消</view>
  165. <view class="confirm-btn" @click="confirmOffShelf">确定</view>
  166. </view>
  167. </view>
  168. </view>
  169. </view>
  170. </template>
  171. <script>
  172. import ListScrollView from '@/components/list-scroll-view/index.vue';
  173. import merchantDishApi, {
  174. STATUS_KEY_TO_TYPE,
  175. STATUS_TEXT,
  176. mapPackageToListItem,
  177. belongingToApi,
  178. getMerchantId
  179. } from '@/api/merchantDish.js';
  180. const STATUS_OPTIONS = [
  181. { label: '全部', value: '' },
  182. { label: '销售中', value: 'selling' },
  183. { label: '已售完', value: 'soldout' },
  184. { label: '待上架', value: 'pending' },
  185. { label: '审核中', value: 'reviewing' },
  186. { label: '未通过', value: 'rejected' }
  187. ];
  188. const TYPE_OPTIONS = [
  189. { label: '全部菜品', value: '' },
  190. { label: '团餐菜品', value: 'group' },
  191. { label: '门店菜品', value: 'store' }
  192. ];
  193. export default {
  194. components: {
  195. ListScrollView
  196. },
  197. data() {
  198. return {
  199. api: {
  200. url: '/groupmeal/gmMerchantPackage/pageListV2',
  201. method: 'POST'
  202. },
  203. keyword: '',
  204. showSearch: false,
  205. batchMode: false,
  206. selectedIds: [],
  207. filterStatus: '',
  208. filterType: '',
  209. filterCategory: '',
  210. categoryOptions: [{ label: '全部品类', value: '' }],
  211. pickerShow: false,
  212. pickerType: '',
  213. pickerList: [],
  214. statusText: STATUS_TEXT,
  215. applyStatus: 0,
  216. listVersion: 0,
  217. listReady: false,
  218. offShelfShow: false,
  219. offShelfItem: null
  220. };
  221. },
  222. computed: {
  223. statusLabel() {
  224. return (STATUS_OPTIONS.find((i) => i.value === this.filterStatus) || {}).label || '全部';
  225. },
  226. typeLabel() {
  227. return (TYPE_OPTIONS.find((i) => i.value === this.filterType) || {}).label || '全部菜品';
  228. },
  229. categoryLabel() {
  230. return (this.categoryOptions.find((i) => i.value === this.filterCategory) || {}).label || '全部品类';
  231. },
  232. initParams() {
  233. const params = {
  234. merchantId: getMerchantId(),
  235. statusType: this.filterStatus ? STATUS_KEY_TO_TYPE[this.filterStatus] : ''
  236. };
  237. if (this.keyword) params.name = this.keyword;
  238. if (this.filterType) params.productBelonging = belongingToApi(this.filterType);
  239. if (this.filterCategory) params.specialCategoryIds = [this.filterCategory];
  240. return params;
  241. },
  242. currentList() {
  243. void this.listVersion;
  244. const scroll = this.$refs.listScroll;
  245. return (scroll && scroll.dataList) || [];
  246. },
  247. isAllSelected() {
  248. const list = this.currentList;
  249. const ids = list.map((i) => i.id);
  250. return ids.length > 0 && ids.every((id) => this.selectedIds.includes(id));
  251. },
  252. bannerVisible() {
  253. return this.applyStatus !== 1;
  254. },
  255. bannerNodes() {
  256. const wrap = (html) => `<span style="color:#B8860B;font-size:14px;">${html}</span>`;
  257. const mark = (word) => `<span style="color:#F5A623;font-weight:600;">${word}</span>`;
  258. if (this.applyStatus == 0) {
  259. return wrap(`提示:平台现已增加团餐活动,如您需要,点击${mark('报名')}参与`);
  260. }
  261. if (this.applyStatus == 3) {
  262. return wrap('提示:团餐活动报名审核中,请耐心等待');
  263. }
  264. if (this.applyStatus == 2) {
  265. return wrap(`提示:团餐报名未通过,点击${mark('重新报名')}`);
  266. }
  267. return '';
  268. }
  269. },
  270. onLoad() {
  271. this.loadSpecialCategories();
  272. },
  273. onShow() {
  274. if (this.listReady) {
  275. this.reloadList();
  276. } else {
  277. this.listReady = true;
  278. }
  279. this.fetchApplyStatus();
  280. },
  281. methods: {
  282. formatPrice(price) {
  283. const n = Number(price);
  284. if (Number.isNaN(n)) return '0.00';
  285. return n.toFixed(2);
  286. },
  287. formatSales(sales) {
  288. const n = Number(sales);
  289. if (Number.isNaN(n)) return '0';
  290. return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  291. },
  292. goBack() {
  293. uni.navigateBack({ delta: 1 });
  294. },
  295. fetchApplyStatus() {
  296. merchantDishApi.isApply().then((res) => {
  297. if (res.data.code !== 200) return;
  298. const result = res.data.result;
  299. let localStatus = 0;
  300. if (result) {
  301. const apiStatus = result.status;
  302. if (apiStatus == 0) localStatus = 3;
  303. else if (apiStatus == 1) localStatus = 1;
  304. else if (apiStatus == 2) localStatus = 2;
  305. }
  306. this.applyStatus = localStatus;
  307. });
  308. },
  309. loadSpecialCategories() {
  310. merchantDishApi.specialCategoryList().then((res) => {
  311. if (res.data.code !== 200) return;
  312. const rows = res.data.result || [];
  313. this.categoryOptions = [
  314. { label: '全部品类', value: '' },
  315. ...rows.map((i) => ({
  316. label: i.categoryName,
  317. value: String(i.id)
  318. }))
  319. ];
  320. });
  321. },
  322. afterFetch(result) {
  323. const records = Array.isArray(result.records) ? result.records : [];
  324. records.forEach((item) => {
  325. Object.assign(item, mapPackageToListItem(item));
  326. });
  327. this.listVersion += 1;
  328. },
  329. reloadList() {
  330. this.$nextTick(() => {
  331. const scroll = this.$refs.listScroll;
  332. if (scroll && scroll.mescroll) {
  333. scroll.reloadList(this.initParams);
  334. }
  335. });
  336. },
  337. onSearch() {
  338. this.reloadList();
  339. },
  340. openPicker(type) {
  341. if (this.batchMode) return;
  342. if (this.pickerShow && this.pickerType === type) {
  343. this.closePicker();
  344. return;
  345. }
  346. this.pickerType = type;
  347. const map = {
  348. status: STATUS_OPTIONS,
  349. type: TYPE_OPTIONS,
  350. category: this.categoryOptions
  351. };
  352. this.pickerList = (map[type] || []).map((i) => ({ text: i.label, value: i.value }));
  353. this.pickerShow = true;
  354. },
  355. closePicker() {
  356. this.pickerShow = false;
  357. this.pickerType = '';
  358. },
  359. isOptionActive(opt) {
  360. const value = opt && opt.value !== undefined ? opt.value : '';
  361. if (this.pickerType === 'status') return this.filterStatus === value;
  362. if (this.pickerType === 'type') return this.filterType === value;
  363. if (this.pickerType === 'category') return this.filterCategory === value;
  364. return false;
  365. },
  366. onPick(opt) {
  367. const value = opt && opt.value !== undefined ? opt.value : '';
  368. if (this.pickerType === 'status') this.filterStatus = value;
  369. if (this.pickerType === 'type') this.filterType = value;
  370. if (this.pickerType === 'category') this.filterCategory = value;
  371. this.selectedIds = [];
  372. this.closePicker();
  373. this.reloadList();
  374. },
  375. enterBatch() {
  376. this.closePicker();
  377. this.batchMode = true;
  378. this.selectedIds = [];
  379. },
  380. exitBatch() {
  381. this.batchMode = false;
  382. this.selectedIds = [];
  383. },
  384. toggleSelect(item) {
  385. if (!this.batchMode) return;
  386. const i = this.selectedIds.indexOf(item.id);
  387. if (i >= 0) this.selectedIds.splice(i, 1);
  388. else this.selectedIds.push(item.id);
  389. },
  390. toggleAll() {
  391. if (this.isAllSelected) this.selectedIds = [];
  392. else this.selectedIds = this.currentList.map((i) => i.id);
  393. },
  394. goCreate() {
  395. uni.navigateTo({ url: '/pages/merchantDish/dish/form' });
  396. },
  397. goEdit(item) {
  398. uni.navigateTo({ url: `/pages/merchantDish/dish/form?id=${item.id}` });
  399. },
  400. onBannerClick() {
  401. if (this.applyStatus === 3) {
  402. uni.showToast({ title: '审核中,请耐心等待', icon: 'none' });
  403. return;
  404. }
  405. uni.navigateTo({ url: '/pages/merchantDish/apply/index' });
  406. },
  407. confirmModal(content) {
  408. return new Promise((resolve) => {
  409. uni.showModal({
  410. title: '提示',
  411. content,
  412. confirmColor: '#333333',
  413. success: (res) => resolve(!!res.confirm)
  414. });
  415. });
  416. },
  417. async onOffShelf(item) {
  418. // 团餐:展示未成团/已成团订单数确认弹窗
  419. if (item.belong === 'group') {
  420. this.offShelfItem = item;
  421. this.offShelfShow = true;
  422. return;
  423. }
  424. if (!(await this.confirmModal('确定下架当前商品'))) return;
  425. this.doOffShelf(item);
  426. },
  427. closeOffShelf() {
  428. this.offShelfShow = false;
  429. this.offShelfItem = null;
  430. },
  431. confirmOffShelf() {
  432. const item = this.offShelfItem;
  433. this.closeOffShelf();
  434. if (item) this.doOffShelf(item);
  435. },
  436. doOffShelf(item) {
  437. merchantDishApi.packageLaunch(item.id, 0).then((res) => {
  438. this.$tip.toast(res.data.message || (res.data.code === 200 ? '已下架' : '操作失败'));
  439. if (res.data.code === 200) this.reloadList();
  440. });
  441. },
  442. async onOnShelf(item) {
  443. if (!(await this.confirmModal('确定上架当前商品'))) return;
  444. merchantDishApi.packageLaunch(item.id, 1).then((res) => {
  445. this.$tip.toast(res.data.message || (res.data.code === 200 ? '已上架' : '操作失败'));
  446. if (res.data.code === 200) this.reloadList();
  447. });
  448. },
  449. async onDelete(item) {
  450. if (!(await this.confirmModal('确定删除当前商品'))) return;
  451. merchantDishApi.packageDelete(item.id).then((res) => {
  452. this.$tip.toast(res.data.message || (res.data.code === 200 ? '已删除' : '操作失败'));
  453. if (res.data.code === 200) this.reloadList();
  454. });
  455. },
  456. ensureSelected() {
  457. if (!this.selectedIds.length) {
  458. uni.showToast({ title: '请先选择菜品', icon: 'none' });
  459. return false;
  460. }
  461. return true;
  462. },
  463. async batchOff() {
  464. if (!this.ensureSelected()) return;
  465. if (!(await this.confirmModal('确定批量下架选中商品?'))) return;
  466. merchantDishApi.packageBatchLaunch(this.selectedIds, 0).then((res) => {
  467. this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量下架成功' : '操作失败'));
  468. if (res.data.code === 200) {
  469. this.exitBatch();
  470. this.reloadList();
  471. }
  472. });
  473. },
  474. async batchOn() {
  475. if (!this.ensureSelected()) return;
  476. if (!(await this.confirmModal('确定批量上架选中商品?'))) return;
  477. merchantDishApi.packageBatchLaunch(this.selectedIds, 1).then((res) => {
  478. this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量上架成功' : '操作失败'));
  479. if (res.data.code === 200) {
  480. this.exitBatch();
  481. this.reloadList();
  482. }
  483. });
  484. },
  485. async batchDelete() {
  486. if (!this.ensureSelected()) return;
  487. if (!(await this.confirmModal('确定批量删除选中商品?'))) return;
  488. merchantDishApi.packageDeleteBatch(this.selectedIds).then((res) => {
  489. this.$tip.toast(res.data.message || (res.data.code === 200 ? '批量删除成功' : '操作失败'));
  490. if (res.data.code === 200) {
  491. this.exitBatch();
  492. this.reloadList();
  493. }
  494. });
  495. }
  496. }
  497. };
  498. </script>
  499. <style lang="scss" scoped>
  500. .page {
  501. height: 100vh;
  502. background: #f7f8fa;
  503. display: flex;
  504. flex-direction: column;
  505. overflow: hidden;
  506. }
  507. .nav-right {
  508. display: flex;
  509. align-items: center;
  510. padding-right: 24rpx;
  511. gap: 28rpx;
  512. }
  513. .nav-icon {
  514. width: 56rpx;
  515. height: 56rpx;
  516. }
  517. .cancel-btn {
  518. font-size: 28rpx;
  519. color: #333;
  520. padding-right: 8rpx;
  521. }
  522. .search-bar {
  523. padding: 12rpx 24rpx;
  524. background: #fff;
  525. flex-shrink: 0;
  526. }
  527. /* ========== 筛选栏 + 下拉面板 ========== */
  528. .filter-wrap {
  529. position: relative;
  530. flex-shrink: 0;
  531. z-index: 30;
  532. }
  533. .filters {
  534. display: flex;
  535. align-items: center;
  536. background: #fff;
  537. padding: 24rpx 0;
  538. position: relative;
  539. z-index: 2;
  540. .filter-item {
  541. flex: 1;
  542. display: flex;
  543. align-items: center;
  544. justify-content: center;
  545. font-size: 28rpx;
  546. color: #666;
  547. &.active {
  548. color: #449aff;
  549. }
  550. .filter-text {
  551. max-width: 180rpx;
  552. overflow: hidden;
  553. text-overflow: ellipsis;
  554. white-space: nowrap;
  555. }
  556. }
  557. .filter-arrow {
  558. width: 18rpx;
  559. height: 18rpx;
  560. margin-left: 8rpx;
  561. flex-shrink: 0;
  562. transition: transform 0.2s;
  563. &.up {
  564. transform: rotate(180deg);
  565. }
  566. }
  567. }
  568. .filter-dropdown {
  569. position: absolute;
  570. left: 0;
  571. right: 0;
  572. top: 100%;
  573. z-index: 1;
  574. }
  575. .filter-panel {
  576. position: relative;
  577. z-index: 2;
  578. background: #fff;
  579. border-radius: 0 0 16rpx 16rpx;
  580. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
  581. padding: 8rpx 0 24rpx;
  582. max-height: 560rpx;
  583. overflow-y: auto;
  584. }
  585. .filter-option {
  586. padding: 22rpx 48rpx;
  587. font-size: 28rpx;
  588. color: #222;
  589. line-height: 1.4;
  590. &.on {
  591. color: #449aff;
  592. }
  593. }
  594. .filter-mask {
  595. position: absolute;
  596. left: 0;
  597. right: 0;
  598. top: 0;
  599. height: 100vh;
  600. z-index: 1;
  601. background: rgba(0, 0, 0, 0.4);
  602. }
  603. .banner {
  604. height: 80rpx;
  605. line-height: 80rpx;
  606. background: #fff8e6;
  607. font-size: 28rpx;
  608. text-align: center;
  609. }
  610. .list-wrap {
  611. flex: 1;
  612. min-height: 0;
  613. box-sizing: border-box;
  614. }
  615. .list {
  616. padding: 16rpx 24rpx;
  617. box-sizing: border-box;
  618. }
  619. /* ========== 菜品卡片 ========== */
  620. .card {
  621. display: flex;
  622. background: #fff;
  623. border-radius: 16rpx;
  624. padding: 24rpx;
  625. margin-bottom: 16rpx;
  626. position: relative;
  627. overflow: hidden;
  628. .belong-tag {
  629. position: absolute;
  630. left: 0;
  631. top: 0;
  632. height: 36rpx;
  633. width: auto;
  634. z-index: 2;
  635. }
  636. .check {
  637. display: flex;
  638. align-items: flex-start;
  639. padding-top: 40rpx;
  640. margin-right: 16rpx;
  641. }
  642. .dot {
  643. width: 36rpx;
  644. height: 36rpx;
  645. border-radius: 50%;
  646. border: 2rpx solid #ccc;
  647. box-sizing: border-box;
  648. display: flex;
  649. align-items: center;
  650. justify-content: center;
  651. &.on {
  652. background: #449aff;
  653. border-color: #449aff;
  654. }
  655. }
  656. .thumb-wrap {
  657. width: 160rpx;
  658. height: 160rpx;
  659. flex-shrink: 0;
  660. .thumb {
  661. width: 100%;
  662. height: 100%;
  663. border-radius: 12rpx;
  664. background: #eee;
  665. }
  666. }
  667. .info {
  668. flex: 1;
  669. margin-left: 20rpx;
  670. min-width: 0;
  671. display: flex;
  672. flex-direction: column;
  673. }
  674. .row-top {
  675. display: flex;
  676. justify-content: space-between;
  677. align-items: flex-start;
  678. .name {
  679. flex: 1;
  680. font-size: 30rpx;
  681. font-weight: 600;
  682. color: #222;
  683. overflow: hidden;
  684. text-overflow: ellipsis;
  685. white-space: nowrap;
  686. padding-right: 12rpx;
  687. }
  688. .status {
  689. font-size: 24rpx;
  690. flex-shrink: 0;
  691. &.selling {
  692. color: #19be6b;
  693. }
  694. &.soldout {
  695. color: #999;
  696. }
  697. &.pending {
  698. color: #ff9900;
  699. }
  700. &.reviewing {
  701. color: #f5a623;
  702. }
  703. &.rejected {
  704. color: #ff4d4f;
  705. }
  706. }
  707. }
  708. .desc {
  709. font-size: 22rpx;
  710. color: #999;
  711. margin-top: 8rpx;
  712. overflow: hidden;
  713. white-space: nowrap;
  714. text-overflow: ellipsis;
  715. }
  716. .meta {
  717. margin-top: 8rpx;
  718. font-size: 22rpx;
  719. color: #999;
  720. .meta-gap {
  721. margin-left: 20rpx;
  722. }
  723. }
  724. .price-line {
  725. margin-top: 10rpx;
  726. .price {
  727. color: #ff4d4f;
  728. font-size: 32rpx;
  729. font-weight: 600;
  730. margin-right: 12rpx;
  731. }
  732. .origin {
  733. font-size: 24rpx;
  734. color: #999;
  735. text-decoration: line-through;
  736. }
  737. }
  738. .actions {
  739. display: flex;
  740. justify-content: flex-end;
  741. align-items: center;
  742. gap: 24rpx;
  743. margin-top: 16rpx;
  744. }
  745. .btn-link {
  746. font-size: 26rpx;
  747. color: #666;
  748. line-height: 52rpx;
  749. padding: 0 4rpx;
  750. }
  751. .btn {
  752. font-size: 26rpx;
  753. padding: 0 28rpx;
  754. height: 52rpx;
  755. line-height: 52rpx;
  756. border-radius: 26rpx;
  757. color: #666;
  758. background: #fff;
  759. &.primary {
  760. background: #449aff;
  761. color: #fff;
  762. }
  763. }
  764. }
  765. .footer {
  766. position: fixed;
  767. left: 0;
  768. right: 0;
  769. bottom: 0;
  770. z-index: 20;
  771. padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
  772. background: #fff;
  773. }
  774. .primary-btn {
  775. height: 88rpx;
  776. line-height: 88rpx;
  777. text-align: center;
  778. color: #fff;
  779. font-size: 32rpx;
  780. border-radius: 44rpx;
  781. background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
  782. }
  783. .batch-footer {
  784. position: fixed;
  785. left: 0;
  786. right: 0;
  787. bottom: 0;
  788. display: flex;
  789. align-items: center;
  790. padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
  791. background: #fff;
  792. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
  793. .all {
  794. display: flex;
  795. align-items: center;
  796. font-size: 26rpx;
  797. margin-right: 20rpx;
  798. .dot {
  799. width: 36rpx;
  800. height: 36rpx;
  801. border-radius: 50%;
  802. border: 2rpx solid #ccc;
  803. margin-right: 10rpx;
  804. box-sizing: border-box;
  805. display: flex;
  806. align-items: center;
  807. justify-content: center;
  808. &.on {
  809. background: #449aff;
  810. border-color: #449aff;
  811. }
  812. }
  813. }
  814. .batch-btns {
  815. flex: 1;
  816. display: flex;
  817. gap: 12rpx;
  818. justify-content: flex-end;
  819. }
  820. .b-btn {
  821. flex: 1;
  822. text-align: center;
  823. height: 72rpx;
  824. line-height: 72rpx;
  825. border-radius: 36rpx;
  826. font-size: 26rpx;
  827. &.danger {
  828. background: #ff4d4f;
  829. color: #fff;
  830. }
  831. &.outline {
  832. border: 1rpx solid #ff4d4f;
  833. color: #ff4d4f;
  834. }
  835. &.disabled {
  836. background: #f0f0f0;
  837. color: #bbb;
  838. pointer-events: none;
  839. }
  840. }
  841. }
  842. .mask {
  843. position: fixed;
  844. left: 0;
  845. right: 0;
  846. top: 0;
  847. bottom: 0;
  848. background: rgba(0, 0, 0, 0.5);
  849. z-index: 1000;
  850. display: flex;
  851. align-items: center;
  852. justify-content: center;
  853. }
  854. .confirm-dialog {
  855. width: 560rpx;
  856. background: #fff;
  857. border-radius: 20rpx;
  858. overflow: hidden;
  859. }
  860. .confirm-title {
  861. padding: 40rpx 40rpx 0;
  862. text-align: center;
  863. font-size: 34rpx;
  864. font-weight: 600;
  865. color: #222;
  866. line-height: 1.4;
  867. }
  868. .confirm-content {
  869. padding: 24rpx 40rpx 48rpx;
  870. text-align: center;
  871. font-size: 28rpx;
  872. color: #333;
  873. line-height: 1.6;
  874. }
  875. .confirm-footer {
  876. display: flex;
  877. border-top: 1rpx solid #e5e5e5;
  878. }
  879. .confirm-btn {
  880. flex: 1;
  881. height: 96rpx;
  882. line-height: 96rpx;
  883. text-align: center;
  884. font-size: 32rpx;
  885. color: #333;
  886. &:first-child {
  887. border-right: 1rpx solid #e5e5e5;
  888. }
  889. }
  890. </style>