| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798 |
- <script setup>
- import { computed, ref, watch } from 'vue';
- import { request, parseResponseJson } from '@/utils/request';
- import { DEFAULT_PLACEHOLDER_IMAGE_URL } from '@/utils/staticAsset';
- import ClientSkeleton from '@/components/client/client-skeleton.vue';
- import ClientImageLoader from '@/components/client/client-image-loader.vue';
- const props = defineProps({
- quotationId: { type: String, default: '' }
- });
- const inquiryId = ref('');
- const status = ref('');
- const submitTime = ref('');
- const loading = ref(false);
- const keyword = ref('');
- const quickPriceFilter = ref('all');
- const selectedFirstCategoryId = ref('all');
- const selectedLeftFilterId = ref('all');
- const items = ref([]);
- const statusText = {
- submitted: '已提交',
- selected: '已采纳'
- };
- function getItemSecondCategoryId(item) {
- const fid = item.firstCategoryId || item.categoryId || '';
- if (item.secondCategoryId)
- return item.secondCategoryId;
- if (item.categoryId && item.categoryId !== fid)
- return item.categoryId;
- return '';
- }
- const firstCategoryCards = computed(() => {
- const map = new Map();
- items.value.forEach((item) => {
- const firstId = item.firstCategoryId || item.categoryId;
- if (!firstId)
- return;
- if (!map.has(firstId)) {
- map.set(firstId, {
- id: firstId,
- name: item.firstCategoryName || item.categoryName || '未分类',
- imageUrl: item.firstCategoryImage || DEFAULT_PLACEHOLDER_IMAGE_URL
- });
- }
- });
- return [{ id: 'all', name: '全部', imageUrl: DEFAULT_PLACEHOLDER_IMAGE_URL }, ...Array.from(map.values())];
- });
- const firstCategoryPages = computed(() => {
- const pages = [];
- const pageSize = 10;
- for (let i = 0; i < firstCategoryCards.value.length; i += pageSize) {
- pages.push(firstCategoryCards.value.slice(i, i + pageSize));
- }
- return pages;
- });
- const firstCategorySwiperIndex = ref(0);
- const leftCategoryOptions = computed(() => {
- if (selectedFirstCategoryId.value === 'all') {
- return firstCategoryCards.value;
- }
- const firstId = selectedFirstCategoryId.value;
- const firstItem = items.value.find((item) => (item.firstCategoryId || item.categoryId) === firstId);
- const firstName = firstItem?.firstCategoryName || firstItem?.categoryName || '未分类';
- const map = new Map();
- items.value.forEach((item) => {
- const fid = item.firstCategoryId || item.categoryId;
- if (fid !== firstId)
- return;
- const secondId = getItemSecondCategoryId(item);
- if (secondId) {
- map.set(secondId, item.secondCategoryName || item.categoryName || '未分类');
- }
- });
- return [
- { id: 'all', name: firstName },
- ...Array.from(map.entries()).map(([id, name]) => ({ id, name }))
- ];
- });
- const filteredItems = computed(() => {
- let list = items.value;
- if (selectedFirstCategoryId.value !== 'all') {
- list = list.filter((item) => (item.firstCategoryId || item.categoryId) === selectedFirstCategoryId.value);
- }
- if (selectedLeftFilterId.value !== 'all') {
- if (selectedFirstCategoryId.value === 'all') {
- list = list.filter((item) => (item.firstCategoryId || item.categoryId) === selectedLeftFilterId.value);
- }
- else {
- list = list.filter((item) => getItemSecondCategoryId(item) === selectedLeftFilterId.value);
- }
- }
- const kw = keyword.value.trim().toLowerCase();
- if (kw) {
- list = list.filter((item) => (item.productName || '').toLowerCase().includes(kw));
- }
- if (quickPriceFilter.value === 'empty') {
- list = list.filter((item) => !(Number(item.price) > 0));
- }
- else if (quickPriceFilter.value === 'filled') {
- list = list.filter((item) => Number(item.price) > 0);
- }
- return list;
- });
- function formatMeasureText(item) {
- const unitText = (item.unitName || '').trim();
- if (item.measureValue == null || item.measureValue === '') {
- return unitText ? `- ${unitText}` : '-';
- }
- const measureNum = Number(item.measureValue);
- if (!Number.isNaN(measureNum)) {
- const measureText = String(measureNum).replace(/\.?0+$/, '');
- return unitText ? `${measureText} ${unitText}` : measureText;
- }
- const measureText = String(item.measureValue).trim();
- return unitText ? `${measureText} ${unitText}` : measureText || '-';
- }
- function handleFirstCategoryClick(categoryId) {
- selectedFirstCategoryId.value = categoryId;
- selectedLeftFilterId.value = 'all';
- }
- function onFirstCategorySwiperChange(e) {
- const cur = e?.detail?.current;
- if (typeof cur === 'number')
- firstCategorySwiperIndex.value = cur;
- }
- function goFirstCategorySwiperPage(index) {
- firstCategorySwiperIndex.value = index;
- }
- watch(() => [selectedFirstCategoryId.value, leftCategoryOptions.value], ([, options]) => {
- const ids = new Set(options.map((o) => o.id));
- if (selectedLeftFilterId.value !== 'all' && !ids.has(selectedLeftFilterId.value)) {
- selectedLeftFilterId.value = 'all';
- }
- });
- watch(() => firstCategoryPages.value.length, () => {
- firstCategorySwiperIndex.value = 0;
- });
- function onSearchClick() {
- uni.hideKeyboard && uni.hideKeyboard();
- }
- function goProductDetail(item) {
- const qid = props.quotationId;
- if (!item.productId || !qid)
- return;
- uni.navigateTo({
- url: `/pages/supplier/product/detail?productId=${item.productId}"ationId=${qid}&readonly=1`
- });
- }
- async function fetchData() {
- const qid = props.quotationId;
- if (!qid) {
- items.value = [];
- return;
- }
- loading.value = true;
- try {
- const quotationRes = await request({
- url: `/mini/supplier/quotations/${qid}`,
- method: 'GET'
- });
- const quotationBody = parseResponseJson(quotationRes);
- if (quotationBody.code !== 200 || !quotationBody.data) {
- uni.showToast({ title: '报价单加载失败', icon: 'none' });
- return;
- }
- const d = quotationBody.data;
- inquiryId.value = d.inquiryId || '';
- status.value = d.status || '';
- submitTime.value = d.submitTime || '';
- const quotationItems = d.items || [];
- const priceMap = quotationItems.reduce((acc, row) => {
- acc[row.productId] = Number(row.price || 0);
- return acc;
- }, {});
- if (!inquiryId.value) {
- items.value = quotationItems.map((row) => ({
- productId: row.productId,
- productName: row.productName,
- price: Number(row.price || 0)
- }));
- selectedFirstCategoryId.value = 'all';
- selectedLeftFilterId.value = 'all';
- return;
- }
- const inquiryRes = await request({
- url: `/mini/supplier/inquiries/${inquiryId.value}`,
- method: 'GET'
- });
- const inquiryBody = parseResponseJson(inquiryRes);
- const inquiryItems = inquiryBody.code === 200 ? (inquiryBody.data && inquiryBody.data.items) || [] : [];
- items.value = inquiryItems
- .filter((row) => row.productId in priceMap)
- .map((row) => ({
- ...row,
- price: Number(priceMap[row.productId] || 0)
- }));
- selectedFirstCategoryId.value = 'all';
- selectedLeftFilterId.value = 'all';
- }
- finally {
- loading.value = false;
- }
- }
- watch(() => props.quotationId, (id) => {
- if (id) {
- void fetchData();
- }
- else {
- items.value = [];
- inquiryId.value = '';
- status.value = '';
- submitTime.value = '';
- }
- }, { immediate: true });
- </script>
- <template>
- <view class="inquiry-page">
- <view v-if="loading" class="state">
- <ClientSkeleton variant="detail" :rows="2" />
- </view>
- <view v-else class="content-wrap">
- <view class="filter-top-wrap">
- <view class="top-row">
- <text class="top-row-left">报价日期:{{ submitTime || '-' }}</text>
- <text class="meta-status" :class="status === 'selected' ? 'selected' : ''">
- {{ statusText[status] || status || '-' }}
- </text>
- </view>
- <view class="search-wrap">
- <view class="search-bar">
- <input
- v-model="keyword"
- class="search-input"
- type="text"
- placeholder="搜索商品名称"
- placeholder-class="search-placeholder"
- confirm-type="search"
- @confirm="onSearchClick"
- />
- <text class="search-action" @click="onSearchClick">搜索</text>
- </view>
- </view>
- </view>
- <view class="category-section">
- <view class="section-head">
- <text class="section-title">商品分类</text>
- </view>
- <view class="first-category-wrap">
- <swiper
- v-if="firstCategoryPages.length > 1"
- class="first-category-swiper"
- :current="firstCategorySwiperIndex"
- :circular="false"
- :indicator-dots="false"
- @change="onFirstCategorySwiperChange"
- >
- <swiper-item v-for="(page, pageIdx) in firstCategoryPages" :key="`category-page-${pageIdx}`">
- <view class="first-category-grid">
- <view
- v-for="cat in page"
- :key="cat.id"
- class="first-category-card"
- :class="{ active: selectedFirstCategoryId === cat.id }"
- hover-class="first-category-card--hover"
- :hover-stay-time="120"
- @click="handleFirstCategoryClick(cat.id)"
- >
- <ClientImageLoader class="first-category-image" :src="cat.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL" mode="aspectFill" radius="24rpx" />
- <text class="first-category-name">{{ cat.name }}</text>
- </view>
- </view>
- </swiper-item>
- </swiper>
- <view v-else class="first-category-grid">
- <view
- v-for="cat in firstCategoryCards"
- :key="cat.id"
- class="first-category-card"
- :class="{ active: selectedFirstCategoryId === cat.id }"
- hover-class="first-category-card--hover"
- :hover-stay-time="120"
- @click="handleFirstCategoryClick(cat.id)"
- >
- <ClientImageLoader class="first-category-image" :src="cat.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL" mode="aspectFill" radius="24rpx" />
- <text class="first-category-name">{{ cat.name }}</text>
- </view>
- </view>
- </view>
- <view v-if="firstCategoryPages.length > 0" class="first-level-indicator">
- <view
- v-for="(_, idx) in firstCategoryPages"
- :key="`dot-${idx}`"
- class="indicator-dot"
- :class="{ active: firstCategorySwiperIndex === idx }"
- @click="goFirstCategorySwiperPage(idx)"
- />
- </view>
- </view>
- <view class="product-layout">
- <view class="product-body">
- <scroll-view scroll-y class="category-pane" :show-scrollbar="true">
- <view
- v-for="(cat, cIdx) in leftCategoryOptions"
- :key="`${selectedFirstCategoryId}-${cIdx}-${cat.id}`"
- class="category-item"
- :class="{ active: selectedLeftFilterId === cat.id }"
- @click="selectedLeftFilterId = cat.id"
- >
- {{ cat.name }}
- </view>
- </scroll-view>
- <view class="goods-pane">
- <view class="quick-tags">
- <view
- class="tag"
- :class="{ active: quickPriceFilter === 'all' }"
- @click="quickPriceFilter = 'all'"
- >
- 全部
- </view>
- <view
- class="tag"
- :class="{ active: quickPriceFilter === 'empty' }"
- @click="quickPriceFilter = 'empty'"
- >
- 价格为空
- </view>
- <view
- class="tag"
- :class="{ active: quickPriceFilter === 'filled' }"
- @click="quickPriceFilter = 'filled'"
- >
- 价格不为空
- </view>
- </view>
- <scroll-view scroll-y class="goods-scroll" :show-scrollbar="true">
- <view class="goods-scroll-inner">
- <view
- v-for="item in filteredItems"
- :key="item.productId"
- class="goods-card"
- hover-class="goods-card--pressed"
- :hover-stay-time="120"
- @click="goProductDetail(item)"
- >
- <ClientImageLoader class="goods-image" :src="item.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL" mode="aspectFill" radius="16rpx" />
- <view class="goods-main">
- <text class="goods-name">{{ item.productName || '-' }}</text>
- <text class="goods-spec">{{ formatMeasureText(item) }}</text>
- <view class="price-row">
- <text class="price-label">报价</text>
- <text class="price-value">{{ Number(item.price || 0).toFixed(2) }}</text>
- <text class="price-unit">元</text>
- </view>
- </view>
- </view>
- <view v-if="filteredItems.length === 0" class="empty-list-tip">暂无匹配商品</view>
- <view class="goods-scroll-spacer" />
- </view>
- </scroll-view>
- </view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <style lang="scss">
- .search-placeholder {
- color: #8aa39a;
- }
- </style>
- <style lang="scss" scoped>
- .inquiry-page {
- height: 100%;
- max-height: 100%;
- min-height: 0;
- background: #f5f8f7;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- }
- .state {
- flex: 1;
- min-height: 0;
- padding: 0;
- }
- .content-wrap {
- flex: 1;
- min-height: 0;
- padding: 0 32rpx;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- }
- .filter-top-wrap {
- flex-shrink: 0;
- background: linear-gradient(145deg, #0a9f6d, #07c160);
- border-bottom-left-radius: 28rpx;
- border-bottom-right-radius: 28rpx;
- padding: 20rpx 24rpx 24rpx;
- margin: 0 -32rpx 0;
- }
- .top-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12rpx;
- gap: 12rpx;
- }
- .top-row-left {
- color: #ffffff;
- font-size: 28rpx;
- font-weight: 600;
- min-width: 0;
- }
- .meta-status {
- padding: 6rpx 16rpx;
- border-radius: 999rpx;
- font-size: 22rpx;
- color: #087f57;
- background: #d1fae5;
- font-weight: 600;
- }
- .meta-status.selected {
- color: #166534;
- background: #bbf7d0;
- }
- .search-wrap {
- margin-top: 8rpx;
- }
- .search-bar {
- display: flex;
- align-items: center;
- height: 72rpx;
- background: #ffffff;
- border: 1rpx solid #d9e9e1;
- border-radius: 36rpx;
- padding: 0 8rpx 0 24rpx;
- box-sizing: border-box;
- }
- .search-input {
- flex: 1;
- min-width: 0;
- height: 72rpx;
- border: none;
- padding: 0;
- font-size: 28rpx;
- color: #303133;
- background: transparent;
- box-sizing: border-box;
- }
- .search-action {
- flex-shrink: 0;
- padding: 0 16rpx 0 8rpx;
- font-size: 24rpx;
- color: #0a9f6d;
- font-weight: 600;
- }
- .category-section {
- flex-shrink: 0;
- margin-top: 24rpx;
- }
- .section-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16rpx;
- }
- .section-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1f2a26;
- }
- .product-layout {
- flex: 1;
- min-height: 0;
- margin-top: 20rpx;
- margin-left: -32rpx;
- margin-right: -32rpx;
- }
- .first-category-wrap {
- background: #ffffff;
- border: 1rpx solid #d9ebe3;
- border-radius: 28rpx;
- padding: 8rpx;
- box-sizing: border-box;
- overflow: hidden;
- }
- .first-category-swiper {
- height: 336rpx;
- }
- .first-category-grid {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- grid-template-rows: repeat(2, minmax(0, 1fr));
- gap: 16rpx;
- padding: 8rpx;
- box-sizing: border-box;
- width: 100%;
- height: 100%;
- align-content: start;
- }
- .first-category-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- min-height: 152rpx;
- padding: 8rpx 4rpx 8rpx;
- border-radius: 28rpx;
- background: transparent;
- border: none;
- box-sizing: border-box;
- transition: opacity 0.15s ease, transform 0.15s ease;
- &.active {
- background: linear-gradient(145deg, #e7f8f0, #f1fbf6);
- color: #0a9f6d;
- font-weight: 600;
- box-shadow: 0 4rpx 8rpx rgba(10, 159, 109, 0.12);
- }
- }
- .first-category-card--hover {
- opacity: 0.88;
- }
- .first-category-image {
- width: 84rpx;
- height: 84rpx;
- border-radius: 24rpx;
- background: linear-gradient(145deg, #dff1e8, #cdebdc);
- flex-shrink: 0;
- }
- .first-category-card.active .first-category-image {
- box-shadow: 0 4rpx 14rpx rgba(10, 159, 109, 0.35);
- }
- .first-category-name {
- margin-top: 8rpx;
- width: 100%;
- font-size: 22rpx;
- line-height: 1.2;
- color: #4f665d;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .first-category-card.active .first-category-name {
- color: #0a9f6d;
- }
- .first-level-indicator {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 16rpx;
- margin: 16rpx auto 0;
- }
- .indicator-dot {
- width: 36rpx;
- height: 8rpx;
- border-radius: 999rpx;
- background: #dbe9e2;
- transition: width 0.2s ease, background 0.2s ease;
- }
- .indicator-dot.active {
- width: 56rpx;
- background: linear-gradient(145deg, #14b883, #0a9f6d);
- }
- .product-body {
- display: flex;
- height: 100%;
- min-height: 0;
- min-height: 880rpx;
- border: 1rpx solid #d9ebe3;
- border-radius: 24rpx;
- overflow: hidden;
- background: #ffffff;
- }
- .category-pane {
- width: 184rpx;
- height: 100%;
- background: #f8fcfa;
- border-radius: 24rpx 0 0 24rpx;
- margin-right: 0;
- border-right: 1rpx solid #edf5f1;
- }
- .category-item {
- min-height: 84rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 16rpx;
- font-size: 24rpx;
- color: #5f726a;
- text-align: center;
- border-left: 6rpx solid transparent;
- background: transparent;
- &.active {
- color: #0a9f6d;
- background: #ffffff;
- border-left-color: #0a9f6d;
- font-weight: 600;
- }
- }
- .goods-pane {
- flex: 1;
- min-width: 0;
- min-height: 0;
- height: 100%;
- background: #ffffff;
- border: 1rpx solid #d9ebe3;
- border-left: none;
- padding: 16rpx 0 0;
- display: flex;
- flex-direction: column;
- }
- .quick-tags {
- margin-bottom: 12rpx;
- padding: 0 20rpx;
- box-sizing: border-box;
- background: transparent;
- display: flex;
- gap: 12rpx;
- justify-content: flex-start;
- flex-wrap: wrap;
- }
- .tag {
- padding: 6rpx 14rpx;
- border-radius: 999rpx;
- font-size: 22rpx;
- color: #303133;
- background: #f2f3f5;
- border: 1rpx solid #f2f3f5;
- &.active {
- color: #ffffff;
- background: #0a9f6d;
- border-color: #0a9f6d;
- font-weight: 600;
- }
- }
- .goods-scroll {
- flex: 1;
- min-height: 0;
- height: 100%;
- }
- .goods-scroll-inner {
- padding: 0 20rpx;
- box-sizing: border-box;
- }
- .goods-scroll-spacer {
- height: calc(160rpx + env(safe-area-inset-bottom));
- flex-shrink: 0;
- }
- .empty-list-tip {
- text-align: center;
- color: #909399;
- font-size: 28rpx;
- padding: 40rpx 0;
- }
- .goods-card {
- display: grid;
- grid-template-columns: minmax(112rpx, 36%) minmax(0, 1fr);
- gap: min(16rpx, 2.5vw);
- align-items: stretch;
- background: #fff;
- padding: 0;
- border: 1rpx solid #d9ebe3;
- border-radius: 18rpx;
- margin-bottom: 16rpx;
- overflow: hidden;
- box-sizing: border-box;
- height: 200rpx;
- }
- .goods-card--pressed {
- opacity: 0.96;
- }
- .goods-image {
- width: 100%;
- align-self: stretch;
- height: 100%;
- min-height: 140rpx;
- border-radius: 16rpx;
- background: linear-gradient(145deg, #f4fbf8, #e4f6ef);
- flex-shrink: 0;
- }
- .goods-main {
- padding: 10rpx 12rpx 10rpx 0;
- flex: 1;
- min-width: 0;
- min-height: 0;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- }
- .goods-name {
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- font-size: 26rpx;
- line-height: 1.35;
- color: #1f2a26;
- font-weight: 500;
- word-break: break-word;
- min-height: 70rpx;
- }
- .goods-spec {
- display: block;
- margin: 2rpx 0 8rpx;
- font-size: 22rpx;
- color: #909399;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .price-row {
- display: flex;
- align-items: center;
- margin-top: auto;
- padding: 6rpx 10rpx;
- background: #f6fbf8;
- border-radius: 12rpx;
- }
- .price-label {
- font-size: 24rpx;
- color: #606266;
- margin-right: 12rpx;
- }
- .price-value {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 24rpx;
- font-weight: 600;
- color: #303133;
- }
- .price-unit {
- margin-left: 12rpx;
- font-size: 24rpx;
- color: #606266;
- }
- </style>
|