search.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>{{ pageId }}</span>
  8. </div>
  9. </div>
  10. <!-- 검색 및 필터 영역 -->
  11. <div class="search--modules type2">
  12. <div class="search--inner">
  13. <div class="form--cont--filter">
  14. <v-select
  15. v-model="searchFilter.category"
  16. :items="categoryOptions"
  17. variant="outlined"
  18. class="custom-select"
  19. label="카테고리"
  20. clearable
  21. >
  22. </v-select>
  23. </div>
  24. <div class="form--cont--filter">
  25. <v-select
  26. v-model="searchFilter.region"
  27. :items="regionOptions"
  28. variant="outlined"
  29. class="custom-select"
  30. label="지역"
  31. clearable
  32. >
  33. </v-select>
  34. </div>
  35. <div class="form--cont--text">
  36. <v-text-field
  37. v-model="searchFilter.keyword"
  38. class="custom-input mini"
  39. style="width: 100%"
  40. placeholder="벤더사명을 입력하세요"
  41. @keyup.enter="handleSearch"
  42. ></v-text-field>
  43. </div>
  44. </div>
  45. <v-btn
  46. class="custom-btn btn-blue mini sch--btn"
  47. @click="handleSearch"
  48. :loading="loading"
  49. >
  50. 검색
  51. </v-btn>
  52. </div>
  53. <!-- 내 승인 요청 현황 -->
  54. <div class="data--list--wrap mb-4">
  55. <div class="section--header">
  56. <h3>내 승인 요청 현황</h3>
  57. <v-btn
  58. class="custom-btn mini btn-outline"
  59. @click="showMyRequests = !showMyRequests"
  60. >
  61. {{ showMyRequests ? "숨기기" : "보기" }}
  62. </v-btn>
  63. </div>
  64. <div v-show="showMyRequests" class="my--requests--wrap">
  65. <div v-if="myRequests.length === 0" class="no-data">
  66. <p>진행 중인 승인 요청이 없습니다.</p>
  67. </div>
  68. <div v-else class="request--cards">
  69. <div
  70. v-for="request in myRequests"
  71. :key="request.SEQ"
  72. class="request--card"
  73. :class="getStatusClass(request.STATUS)"
  74. >
  75. <div class="card--header">
  76. <h4>{{ request.vendorName }}</h4>
  77. <v-chip :color="getStatusColor(request.STATUS)" size="small">
  78. {{ getStatusText(request.STATUS) }}
  79. </v-chip>
  80. </div>
  81. <div class="card--content">
  82. <p class="request--date">요청일: {{ formatDate(request.REQUEST_DATE) }}</p>
  83. <p v-if="request.STATUS === 'PENDING'" class="expire--date">
  84. 만료일: {{ formatDate(request.EXPIRED_DATE) }}
  85. </p>
  86. <p v-if="request.REQUEST_MESSAGE" class="request--message">
  87. "{{ request.REQUEST_MESSAGE }}"
  88. </p>
  89. </div>
  90. <div class="card--actions">
  91. <v-btn
  92. v-if="request.STATUS === 'PENDING'"
  93. class="custom-btn mini btn-outline-red"
  94. @click="cancelRequest(request.SEQ)"
  95. size="small"
  96. >
  97. 취소
  98. </v-btn>
  99. <v-btn
  100. class="custom-btn mini btn-outline"
  101. @click="viewRequestDetail(request.SEQ)"
  102. size="small"
  103. >
  104. 상세보기
  105. </v-btn>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- 벤더사 검색 결과 -->
  112. <div class="data--list--wrap">
  113. <div class="btn--actions--wrap">
  114. <div class="left--sections">
  115. <span class="result-count">
  116. 총 {{ pagination.totalCount || 0 }}개의 벤더사
  117. </span>
  118. </div>
  119. <div class="right--sections">
  120. <v-select
  121. v-model="sortOption"
  122. :items="sortOptions"
  123. variant="outlined"
  124. class="custom-select mini"
  125. @update:model-value="handleSort"
  126. >
  127. </v-select>
  128. </div>
  129. </div>
  130. <!-- 로딩 상태 -->
  131. <div v-if="loading" class="loading-wrap">
  132. <v-progress-circular indeterminate color="primary"></v-progress-circular>
  133. <p>벤더사를 검색하고 있습니다...</p>
  134. </div>
  135. <!-- 에러 상태 -->
  136. <div v-else-if="error" class="error-wrap">
  137. <v-alert type="error" dismissible @click:close="error = null">
  138. {{ error }}
  139. </v-alert>
  140. </div>
  141. <!-- 벤더사 리스트 -->
  142. <div v-else-if="vendors.length > 0" class="vendor--search--wrap">
  143. <div class="vendor--grid">
  144. <div v-for="vendor in vendors" :key="vendor.SEQ" class="vendor--card">
  145. <div class="vendor--card--header">
  146. <div class="vendor--logo">
  147. <v-img
  148. v-if="vendor.LOGO"
  149. :src="vendor.LOGO"
  150. :alt="vendor.COMPANY_NAME + ' 로고'"
  151. width="60"
  152. height="60"
  153. ></v-img>
  154. <div v-else class="no-logo">
  155. {{ vendor.COMPANY_NAME?.charAt(0) || "V" }}
  156. </div>
  157. </div>
  158. <div class="vendor--info">
  159. <h3>{{ vendor.COMPANY_NAME }}</h3>
  160. <p class="vendor--category">{{ getCategoryText(vendor.CATEGORY) }}</p>
  161. <div class="vendor--meta">
  162. <span v-if="vendor.REGION">📍 {{ vendor.REGION }}</span>
  163. <span v-if="vendor.PARTNERSHIP_COUNT"
  164. >🤝 {{ vendor.PARTNERSHIP_COUNT }}개 파트너십</span
  165. >
  166. </div>
  167. </div>
  168. </div>
  169. <div class="vendor--card--body">
  170. <p v-if="vendor.DESCRIPTION" class="vendor--description">
  171. {{ vendor.DESCRIPTION }}
  172. </p>
  173. <div class="vendor--tags" v-if="vendor.TAGS">
  174. <v-chip
  175. v-for="tag in vendor.TAGS.split(',')"
  176. :key="tag"
  177. size="small"
  178. variant="outlined"
  179. class="mr-1 mb-1"
  180. >
  181. {{ tag.trim() }}
  182. </v-chip>
  183. </div>
  184. </div>
  185. <div class="vendor--card--footer">
  186. <div class="partnership--status">
  187. <span
  188. v-if="getPartnershipStatus(vendor.SEQ)"
  189. :class="[
  190. 'status-badge',
  191. getPartnershipStatus(vendor.SEQ)?.toLowerCase() || 'unknown',
  192. ]"
  193. >
  194. {{ getPartnershipStatusText(vendor.SEQ) }}
  195. </span>
  196. <span v-else class="status-badge available">신규 파트너십 가능</span>
  197. </div>
  198. <div class="card--actions">
  199. <v-btn
  200. class="custom-btn mini btn-outline mr-2"
  201. @click="viewVendorDetail(vendor.SEQ)"
  202. >
  203. 상세보기
  204. </v-btn>
  205. <v-btn
  206. v-if="showRequestButton(vendor.SEQ)"
  207. class="custom-btn mini btn-blue"
  208. @click="openRequestModal(vendor)"
  209. >
  210. 승인요청
  211. </v-btn>
  212. <v-chip
  213. v-else-if="getPartnershipStatus(vendor.SEQ) === 'APPROVED'"
  214. color="success"
  215. size="small"
  216. >
  217. 승인완료
  218. </v-chip>
  219. </div>
  220. </div>
  221. </div>
  222. </div>
  223. <!-- 페이지네이션 -->
  224. <div class="pagination-wrap" v-if="pagination.totalPages > 1">
  225. <v-pagination
  226. v-model="currentPage"
  227. :length="pagination.totalPages"
  228. :total-visible="7"
  229. @update:model-value="handlePageChange"
  230. ></v-pagination>
  231. </div>
  232. </div>
  233. <!-- 검색 결과 없음 -->
  234. <div v-else class="no-data-wrap">
  235. <div class="no-data">
  236. <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
  237. <h3>검색된 벤더사가 없습니다</h3>
  238. <p>다른 검색 조건을 시도해보세요</p>
  239. </div>
  240. </div>
  241. </div>
  242. <!-- 승인요청 모달 -->
  243. <v-dialog v-model="requestModal.show" max-width="600px">
  244. <v-card>
  245. <v-card-title class="text-h5">
  246. {{ requestModal.vendor?.COMPANY_NAME }}에 승인요청
  247. </v-card-title>
  248. <v-card-text>
  249. <div class="request--form">
  250. <div class="vendor--summary">
  251. <div class="vendor--logo--small">
  252. <v-img
  253. v-if="requestModal.vendor?.LOGO"
  254. :src="requestModal.vendor.LOGO"
  255. width="40"
  256. height="40"
  257. ></v-img>
  258. <div v-else class="no-logo--small">
  259. {{ requestModal.vendor?.COMPANY_NAME?.charAt(0) || "V" }}
  260. </div>
  261. </div>
  262. <div>
  263. <h4>{{ requestModal.vendor?.COMPANY_NAME }}</h4>
  264. <p>{{ getCategoryText(requestModal.vendor?.CATEGORY) }}</p>
  265. </div>
  266. </div>
  267. <v-textarea
  268. v-model="requestModal.message"
  269. label="요청 메시지"
  270. placeholder="벤더사에 전달할 메시지를 작성해주세요..."
  271. rows="4"
  272. counter="500"
  273. maxlength="500"
  274. class="mt-4"
  275. ></v-textarea>
  276. <div class="form--section">
  277. <h5>희망 조건 (선택사항)</h5>
  278. <v-text-field
  279. v-model="requestModal.commissionRate"
  280. label="희망 수수료율 (%)"
  281. type="number"
  282. min="0"
  283. max="100"
  284. step="0.1"
  285. class="mt-2"
  286. ></v-text-field>
  287. <v-textarea
  288. v-model="requestModal.specialConditions"
  289. label="특별 조건"
  290. placeholder="기타 협업 조건이나 요청사항을 입력해주세요..."
  291. rows="3"
  292. class="mt-2"
  293. ></v-textarea>
  294. </div>
  295. </div>
  296. </v-card-text>
  297. <v-card-actions>
  298. <v-spacer></v-spacer>
  299. <v-btn color="grey" variant="text" @click="closeRequestModal"> 취소 </v-btn>
  300. <v-btn color="primary" @click="submitRequest" :loading="submitting">
  301. 승인요청
  302. </v-btn>
  303. </v-card-actions>
  304. </v-card>
  305. </v-dialog>
  306. </div>
  307. </template>
  308. <script setup>
  309. import { ref, onMounted, computed } from "vue";
  310. import { useRouter } from "vue-router";
  311. /************************************************************************
  312. | 레이아웃
  313. ************************************************************************/
  314. definePageMeta({
  315. layout: "default",
  316. });
  317. /************************************************************************
  318. | 스토어 & 라우터
  319. ************************************************************************/
  320. const router = useRouter();
  321. const { $toast } = useNuxtApp();
  322. /************************************************************************
  323. | 반응형 데이터
  324. ************************************************************************/
  325. const pageId = ref("벤더사 검색");
  326. const loading = ref(false);
  327. const submitting = ref(false);
  328. const error = ref(null);
  329. const currentPage = ref(1);
  330. const showMyRequests = ref(false);
  331. // 검색 필터
  332. const searchFilter = ref({
  333. keyword: "",
  334. category: "",
  335. region: "",
  336. });
  337. // 정렬 옵션
  338. const sortOption = ref("latest");
  339. const sortOptions = ref([
  340. { title: "최신순", value: "latest" },
  341. { title: "파트너십 많은순", value: "partnership" },
  342. { title: "이름순", value: "name" },
  343. ]);
  344. // 카테고리 옵션
  345. const categoryOptions = ref([
  346. { title: "전체", value: "" },
  347. { title: "패션·뷰티", value: "FASHION_BEAUTY" },
  348. { title: "식품·건강", value: "FOOD_HEALTH" },
  349. { title: "라이프스타일", value: "LIFESTYLE" },
  350. { title: "테크·가전", value: "TECH_ELECTRONICS" },
  351. { title: "스포츠·레저", value: "SPORTS_LEISURE" },
  352. { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
  353. ]);
  354. // 지역 옵션
  355. const regionOptions = ref([
  356. { title: "전체", value: "" },
  357. { title: "서울", value: "SEOUL" },
  358. { title: "경기", value: "GYEONGGI" },
  359. { title: "인천", value: "INCHEON" },
  360. { title: "부산", value: "BUSAN" },
  361. { title: "대구", value: "DAEGU" },
  362. { title: "대전", value: "DAEJEON" },
  363. { title: "광주", value: "GWANGJU" },
  364. { title: "울산", value: "ULSAN" },
  365. { title: "기타", value: "OTHER" },
  366. ]);
  367. // 데이터
  368. const vendors = ref([]);
  369. const myRequests = ref([]);
  370. const pagination = ref({
  371. currentPage: 1,
  372. totalPages: 1,
  373. totalCount: 0,
  374. pageSize: 12,
  375. });
  376. // 승인요청 모달
  377. const requestModal = ref({
  378. show: false,
  379. vendor: null,
  380. message: "",
  381. commissionRate: null,
  382. specialConditions: "",
  383. });
  384. /************************************************************************
  385. | computed
  386. ************************************************************************/
  387. const currentUser = computed(() => {
  388. // 실제로는 인증 스토어에서 가져옴
  389. return JSON.parse(localStorage.getItem("authStore"))?.auth || {};
  390. });
  391. /************************************************************************
  392. | 메서드
  393. ************************************************************************/
  394. const handleSearch = async () => {
  395. currentPage.value = 1;
  396. await loadVendors();
  397. };
  398. const handlePageChange = async (page) => {
  399. currentPage.value = page;
  400. await loadVendors();
  401. };
  402. const handleSort = async () => {
  403. currentPage.value = 1;
  404. await loadVendors();
  405. };
  406. const loadVendors = async () => {
  407. try {
  408. loading.value = true;
  409. error.value = null;
  410. const params = {
  411. keyword: searchFilter.value.keyword,
  412. category: searchFilter.value.category,
  413. region: searchFilter.value.region,
  414. sortBy: sortOption.value,
  415. page: currentPage.value,
  416. size: pagination.value.pageSize,
  417. influencerSeq: currentUser.value.seq,
  418. };
  419. useAxios()
  420. .post("/api/vendor/search", params)
  421. .then((res) => {
  422. if (res.data.success) {
  423. vendors.value = res.data.data.items;
  424. pagination.value = res.data.data.pagination;
  425. } else {
  426. error.value = res.data.message || "벤더사 검색 중 오류가 발생했습니다.";
  427. }
  428. })
  429. .catch((err) => {
  430. error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
  431. })
  432. .finally(() => {
  433. loading.value = false;
  434. });
  435. } catch (err) {
  436. error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
  437. loading.value = false;
  438. }
  439. };
  440. const loadMyRequests = async () => {
  441. try {
  442. const params = {
  443. influencerSeq: currentUser.value.seq,
  444. // status 파라미터 제거하여 모든 상태의 요청을 로드
  445. };
  446. useAxios()
  447. .post("/api/vendor-influencer/list", params)
  448. .then((res) => {
  449. if (res.data.success) {
  450. myRequests.value = res.data.data.items;
  451. }
  452. })
  453. .catch((err) => {
  454. console.error("내 요청 목록 로드 오류:", err);
  455. });
  456. } catch (err) {
  457. console.error("내 요청 목록 로드 오류:", err);
  458. }
  459. };
  460. const openRequestModal = (vendor) => {
  461. requestModal.value = {
  462. show: true,
  463. vendor: vendor,
  464. message: "",
  465. commissionRate: null,
  466. specialConditions: "",
  467. };
  468. };
  469. const closeRequestModal = () => {
  470. requestModal.value = {
  471. show: false,
  472. vendor: null,
  473. message: "",
  474. commissionRate: null,
  475. specialConditions: "",
  476. };
  477. };
  478. const submitRequest = async () => {
  479. try {
  480. submitting.value = true;
  481. const params = {
  482. vendorSeq: requestModal.value.vendor.SEQ,
  483. influencerSeq: currentUser.value.seq,
  484. requestType: "INFLUENCER_REQUEST",
  485. requestMessage: requestModal.value.message,
  486. requestedBy: currentUser.value.seq,
  487. commissionRate: requestModal.value.commissionRate,
  488. specialConditions: requestModal.value.specialConditions,
  489. };
  490. useAxios()
  491. .post("/api/vendor-influencer/request", params)
  492. .then((res) => {
  493. if (res.data.success) {
  494. $toast.success("승인요청이 성공적으로 전송되었습니다.");
  495. closeRequestModal();
  496. loadMyRequests();
  497. loadVendors();
  498. } else {
  499. $toast.error(res.data.message || "승인요청 전송 중 오류가 발생했습니다.");
  500. }
  501. })
  502. .catch((err) => {
  503. $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
  504. })
  505. .finally(() => {
  506. submitting.value = false;
  507. });
  508. } catch (err) {
  509. $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
  510. submitting.value = false;
  511. }
  512. };
  513. const cancelRequest = async (requestSeq) => {
  514. if (!confirm("승인요청을 취소하시겠습니까?")) return;
  515. try {
  516. const params = {
  517. mappingSeq: requestSeq,
  518. cancelledBy: currentUser.value.SEQ,
  519. cancelReason: "사용자에 의한 취소",
  520. };
  521. useAxios()
  522. .post("/api/vendor-influencer/cancel", params)
  523. .then((res) => {
  524. if (res.data.success) {
  525. $toast.success("승인요청이 취소되었습니다.");
  526. loadMyRequests();
  527. } else {
  528. $toast.error(res.data.message || "요청 취소 중 오류가 발생했습니다.");
  529. }
  530. })
  531. .catch((err) => {
  532. $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
  533. });
  534. } catch (err) {
  535. $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
  536. }
  537. };
  538. const viewVendorDetail = (vendorSeq) => {
  539. router.push(`/view/vendor/${vendorSeq}`);
  540. };
  541. const viewRequestDetail = (requestSeq) => {
  542. router.push(`/view/vendor/request/${requestSeq}`);
  543. };
  544. // 유틸리티 함수들
  545. const getCategoryText = (category) => {
  546. const categoryMap = {
  547. FASHION_BEAUTY: "패션·뷰티",
  548. FOOD_HEALTH: "식품·건강",
  549. LIFESTYLE: "라이프스타일",
  550. TECH_ELECTRONICS: "테크·가전",
  551. SPORTS_LEISURE: "스포츠·레저",
  552. CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
  553. };
  554. return categoryMap[category] || category || "기타";
  555. };
  556. const getStatusText = (status) => {
  557. const statusMap = {
  558. PENDING: "대기중",
  559. APPROVED: "승인완료",
  560. REJECTED: "거절됨",
  561. CANCELLED: "취소됨",
  562. };
  563. return statusMap[status] || status || "알 수 없음";
  564. };
  565. const getStatusColor = (status) => {
  566. const colorMap = {
  567. PENDING: "orange",
  568. APPROVED: "success",
  569. REJECTED: "error",
  570. CANCELLED: "grey",
  571. };
  572. return colorMap[status] || "grey";
  573. };
  574. const getStatusClass = (status) => {
  575. return `status-${status?.toLowerCase() || "unknown"}`;
  576. };
  577. const getPartnershipStatus = (vendorSeq) => {
  578. // 현재 인플루언서의 해당 벤더사에 대한 파트너십 상태 확인
  579. const request = myRequests.value.find(req => req.VENDOR_SEQ === vendorSeq);
  580. return request ? request.STATUS : null;
  581. };
  582. const getPartnershipStatusText = (vendorSeq) => {
  583. const status = getPartnershipStatus(vendorSeq);
  584. const statusMap = {
  585. PENDING: "승인 대기중",
  586. APPROVED: "승인 완료",
  587. REJECTED: "승인 거절됨",
  588. CANCELLED: "요청 취소됨"
  589. };
  590. return statusMap[status] || "신규 파트너십 가능";
  591. };
  592. // 승인요청 버튼 표시 여부 확인
  593. const showRequestButton = (vendorSeq) => {
  594. const status = getPartnershipStatus(vendorSeq);
  595. return !status || status === 'REJECTED' || status === 'CANCELLED';
  596. };
  597. const formatDate = (dateString) => {
  598. return new Date(dateString).toLocaleDateString("ko-KR");
  599. };
  600. /************************************************************************
  601. | 라이프사이클
  602. ************************************************************************/
  603. onMounted(async () => {
  604. await Promise.all([loadVendors(), loadMyRequests()]);
  605. });
  606. </script>
  607. <style scoped>
  608. .section--header {
  609. display: flex;
  610. justify-content: space-between;
  611. align-items: center;
  612. margin-bottom: 16px;
  613. padding-bottom: 8px;
  614. border-bottom: 1px solid #e0e0e0;
  615. }
  616. .my--requests--wrap {
  617. background: #f8f9fa;
  618. border-radius: 8px;
  619. padding: 16px;
  620. margin-bottom: 20px;
  621. }
  622. .request--cards {
  623. display: grid;
  624. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  625. gap: 16px;
  626. }
  627. .request--card {
  628. background: white;
  629. border-radius: 8px;
  630. padding: 16px;
  631. border-left: 4px solid #e0e0e0;
  632. }
  633. .request--card.status-pending {
  634. border-left-color: #ff9800;
  635. }
  636. .request--card.status-approved {
  637. border-left-color: #4caf50;
  638. }
  639. .request--card.status-rejected {
  640. border-left-color: #f44336;
  641. }
  642. .card--header {
  643. display: flex;
  644. justify-content: space-between;
  645. align-items: center;
  646. margin-bottom: 12px;
  647. }
  648. .card--header h4 {
  649. margin: 0;
  650. font-size: 16px;
  651. font-weight: 600;
  652. }
  653. .card--content {
  654. margin-bottom: 12px;
  655. }
  656. .card--content p {
  657. margin: 4px 0;
  658. font-size: 14px;
  659. color: #666;
  660. }
  661. .request--message {
  662. font-style: italic;
  663. color: #333 !important;
  664. }
  665. .card--actions {
  666. display: flex;
  667. gap: 8px;
  668. }
  669. .vendor--search--wrap {
  670. margin-top: 20px;
  671. }
  672. .vendor--grid {
  673. display: grid;
  674. grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
  675. gap: 20px;
  676. margin-bottom: 20px;
  677. }
  678. .vendor--card {
  679. background: white;
  680. border-radius: 12px;
  681. padding: 20px;
  682. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  683. transition: transform 0.2s, box-shadow 0.2s;
  684. }
  685. .vendor--card:hover {
  686. transform: translateY(-2px);
  687. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  688. }
  689. .vendor--card--header {
  690. display: flex;
  691. gap: 16px;
  692. margin-bottom: 16px;
  693. }
  694. .vendor--logo {
  695. width: 60px;
  696. height: 60px;
  697. border-radius: 8px;
  698. overflow: hidden;
  699. flex-shrink: 0;
  700. display: flex;
  701. align-items: center;
  702. justify-content: center;
  703. background: #f5f5f5;
  704. }
  705. .no-logo {
  706. font-size: 24px;
  707. font-weight: bold;
  708. color: #666;
  709. }
  710. .vendor--info h3 {
  711. margin: 0 0 4px 0;
  712. font-size: 18px;
  713. font-weight: 600;
  714. }
  715. .vendor--category {
  716. color: #666;
  717. font-size: 14px;
  718. margin: 0 0 8px 0;
  719. }
  720. .vendor--meta {
  721. display: flex;
  722. flex-direction: column;
  723. gap: 4px;
  724. }
  725. .vendor--meta span {
  726. font-size: 12px;
  727. color: #888;
  728. }
  729. .vendor--card--body {
  730. margin-bottom: 16px;
  731. }
  732. .vendor--description {
  733. font-size: 14px;
  734. color: #666;
  735. line-height: 1.4;
  736. margin-bottom: 12px;
  737. }
  738. .vendor--tags {
  739. margin-bottom: 8px;
  740. }
  741. .vendor--card--footer {
  742. display: flex;
  743. justify-content: space-between;
  744. align-items: center;
  745. }
  746. .partnership--status {
  747. flex: 1;
  748. }
  749. .status-badge {
  750. padding: 4px 8px;
  751. border-radius: 4px;
  752. font-size: 12px;
  753. font-weight: 500;
  754. }
  755. .status-badge.available {
  756. background: #e8f5e8;
  757. color: #2e7d32;
  758. }
  759. .status-badge.pending {
  760. background: #fff3e0;
  761. color: #ef6c00;
  762. }
  763. .status-badge.approved {
  764. background: #e8f5e8;
  765. color: #2e7d32;
  766. }
  767. .status-badge.rejected {
  768. background: #ffebee;
  769. color: #c62828;
  770. }
  771. .card--actions {
  772. display: flex;
  773. gap: 8px;
  774. }
  775. .loading-wrap,
  776. .error-wrap,
  777. .no-data-wrap {
  778. display: flex;
  779. flex-direction: column;
  780. align-items: center;
  781. justify-content: center;
  782. padding: 60px 20px;
  783. }
  784. .no-data {
  785. text-align: center;
  786. }
  787. .no-data h3 {
  788. margin: 16px 0 8px;
  789. color: #666;
  790. }
  791. .no-data p {
  792. color: #999;
  793. }
  794. .pagination-wrap {
  795. display: flex;
  796. justify-content: center;
  797. margin-top: 20px;
  798. }
  799. .request--form {
  800. padding: 8px 0;
  801. }
  802. .vendor--summary {
  803. display: flex;
  804. align-items: center;
  805. gap: 12px;
  806. padding: 12px;
  807. background: #f8f9fa;
  808. border-radius: 8px;
  809. margin-bottom: 16px;
  810. }
  811. .vendor--logo--small {
  812. width: 40px;
  813. height: 40px;
  814. border-radius: 6px;
  815. overflow: hidden;
  816. flex-shrink: 0;
  817. display: flex;
  818. align-items: center;
  819. justify-content: center;
  820. background: #f5f5f5;
  821. }
  822. .no-logo--small {
  823. font-size: 16px;
  824. font-weight: bold;
  825. color: #666;
  826. }
  827. .form--section {
  828. margin-top: 16px;
  829. }
  830. .form--section h5 {
  831. margin: 0 0 8px 0;
  832. font-size: 14px;
  833. font-weight: 600;
  834. color: #333;
  835. }
  836. .result-count {
  837. font-size: 14px;
  838. color: #666;
  839. font-weight: 500;
  840. }
  841. </style>