feat: 添加项目库组件并优化搜索页面样式和功能

新增项目库组件item-project,包含学校、专业、学费等信息的展示
优化搜索页面样式,调整分类项宽度和间距
修复搜索页面滚动定位问题,完善分页和URL参数处理
添加本地开发服务器脚本serve.ps1
更新public.css和public.less样式文件
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-11-20 19:11:48 +08:00
parent c9e229a5cd
commit 40d06c180f
14 changed files with 558 additions and 124 deletions

View File

@@ -0,0 +1,91 @@
const { defineComponent, ref, onMounted } = Vue;
export const itemProject = defineComponent({
name: "item-project",
props: {
itemdata: {
type: Object,
default: () => ({}),
},
},
setup(props) {
const formatNumberWithSpaces = (number) => {
if (Number(number) != number) return;
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const judgmentClass = (name) => {
const redtag = redtagArr.value || ["26Fall", "26Spring"];
let classname = "";
if (redtag.includes(name)) classname = "gray semester";
else {
// 判断 字符串 是以 25/26/27... + Fall/Spring/Summer
const regex = /^(2[0-9]|3\d)(Fall|Spring|Summer)$/;
classname = regex.test(name) ? "gray" : "";
}
return {
name,
class: classname,
};
};
onMounted(() => {
checkWConfig();
});
let redtagArr = ref([]);
const checkWConfig = () => {
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
if (wConfig.time) {
const time = new Date(wConfig.time);
const now = new Date();
if (now - time > 24 * 60 * 60 * 1000) {
getWConfig();
monitorGetRedTag();
} else {
const config = wConfig.config || {};
redtagArr.value = config.redtag || [];
}
} else {
getWConfig();
monitorGetRedTag();
}
};
const monitorGetRedTag = () => {
let timer = setInterval(() => {
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
if (wConfig.time) {
const config = wConfig.config || {};
redtagArr.value = config.redtag;
clearInterval(timer);
}
}, 1000);
};
const getWConfig = () => {
if (window.wConfigloading) return;
window.wConfigloading = true;
ajaxGet("/v2/api/config/website").then((res) => {
if (res.code == 200) {
let data = res["data"] || {};
const config = data.config || {};
redtagArr.value = config.redtag;
data.time = new Date().toISOString();
localStorage.setItem("wConfig", JSON.stringify(data));
}
window.wConfigloading = false;
});
};
let item = ref({ ...props.itemdata });
item.value["tuition_fee_text"] = formatNumberWithSpaces(item.value.tuition_fee || "");
item.value["url"] = "https://program.gter.net/details/" + item.value.uniqid;
return { item };
},
template: `<div class="item-box item-project"> <div class="tag flexflex"> <img class="tag-item icon" v-if="item.admissionsproject" src="https://app.gter.net/image/miniApp/offer/admissions-icon.png" /> <a class="tag-item blue" href="https://program.gter.net/" target="_blank">港校项目库</a> <div class="tag-item" :class="item.class" v-for="(tag, index) in item.tags" :key="index">{{ tag.name || tag }} </div> </div> <a class="school flexacenter" :href="item.url" target="_blank"> <img class="icon" v-if="item.schoollogo" :src="item.schoollogo" /> <span class="flex1">{{ item.schoolname }}</span> </a> <a class="name flexacenter" :href="item.url" target="_blank">{{ item.name_zh || item.program_zh }}</a> <a class="en flexacenter" :href="item.url" target="_blank">{{ item.name_en || item.program_en }}</a> <a class="introduce flexacenter" :href="item.url" target="_blank"> <span>{{ item.department }}</span> <div class="flexacenter" v-if="item.rank"> <div class="line" v-if="item.department">|</div> 专业排名 <div class="q">{{ item.rank }}</div> </div> <div class="flexacenter" v-if="item.tuition_fee"> <div class="line" v-if="item.department || item.rank">|</div> 学费HK$ <div class="q">{{ item.tuition_fee_text }}</div> </div> </a> <a class="word flexacenter one-line-display" v-if="item.distinctive" :href="item.url" target="_blank">{{ item.distinctive }}</a></div>`,
});

View File

@@ -0,0 +1,30 @@
<div class="item-box item-project">
<div class="tag flexflex">
<img class="tag-item icon" v-if="item.admissionsproject" src="https://app.gter.net/image/miniApp/offer/admissions-icon.png" />
<a class="tag-item blue" href="https://program.gter.net/" target="_blank">港校项目库</a>
<div class="tag-item" :class="item.class" v-for="(tag, index) in item.tags" :key="index">{{ tag.name || tag }}
</div>
</div>
<a class="school flexacenter" :href="item.url" target="_blank">
<img class="icon" v-if="item.schoollogo" :src="item.schoollogo" />
<span class="flex1">{{ item.schoolname }}</span>
</a>
<a class="name flexacenter" :href="item.url" target="_blank">{{ item.name_zh || item.program_zh }}</a>
<a class="en flexacenter" :href="item.url" target="_blank">{{ item.name_en || item.program_en }}</a>
<a class="introduce flexacenter" :href="item.url" target="_blank">
<span>{{ item.department }}</span>
<div class="flexacenter" v-if="item.rank">
<div class="line" v-if="item.department">|</div>
专业排名 <div class="q">{{ item.rank }}</div>
</div>
<div class="flexacenter" v-if="item.tuition_fee">
<div class="line" v-if="item.department || item.rank">|</div>
学费HK$ <div class="q">{{ item.tuition_fee_text }}</div>
</div>
</a>
<a class="word flexacenter one-line-display" v-if="item.distinctive" :href="item.url" target="_blank">{{
item.distinctive }}</a>
</div>

View File

@@ -17,6 +17,8 @@
padding-top: 39px; padding-top: 39px;
padding-bottom: 38px; padding-bottom: 38px;
margin-right: 20px; margin-right: 20px;
position: sticky;
top: 10px;
} }
#homepage-me .matter .card-user .avatar-box { #homepage-me .matter .card-user .avatar-box {
position: relative; position: relative;

View File

@@ -16,6 +16,9 @@
padding-top: 39px; padding-top: 39px;
padding-bottom: 38px; padding-bottom: 38px;
margin-right: 20px; margin-right: 20px;
position: sticky;
top: 10px;
.avatar-box { .avatar-box {
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 20px;
@@ -165,7 +168,7 @@
a { a {
text-decoration: none; text-decoration: none;
} }
.bi-url { .bi-url {
text-decoration: underline; text-decoration: underline;
color: #04b0d5; color: #04b0d5;

View File

@@ -38,6 +38,8 @@
padding-top: 39px; padding-top: 39px;
padding-bottom: 40px; padding-bottom: 40px;
margin-right: 20px; margin-right: 20px;
position: sticky;
top: 10px;
} }
#homepage-other .matter .card-user .avatar { #homepage-other .matter .card-user .avatar {
width: 120px; width: 120px;

View File

@@ -40,6 +40,8 @@
padding-top: 39px; padding-top: 39px;
padding-bottom: 40px; padding-bottom: 40px;
margin-right: 20px; margin-right: 20px;
position: sticky;
top: 10px;
.avatar { .avatar {
width: 120px; width: 120px;

View File

@@ -600,6 +600,103 @@ body {
.item-box.item-tenement .picture .picture-item:not(:last-child) { .item-box.item-tenement .picture .picture-item:not(:last-child) {
margin-right: 10px; margin-right: 10px;
} }
.item-box.item-project {
padding-bottom: 18px;
}
.item-box.item-project .school {
color: #333333;
font-size: 14px;
margin-top: 2px;
margin-bottom: 5px;
}
.item-box.item-project .school .icon {
width: 18px;
height: 20px;
margin-right: 8px;
}
.item-box.item-project .name {
word-break: break-word;
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
font-weight: 650;
font-style: normal;
font-size: 20px;
color: #000000;
line-height: 36px;
}
.item-box.item-project .en {
font-size: 14px;
color: #7f7f7f;
margin-top: 4px;
word-break: break-all;
}
.item-box.item-project .introduce {
font-size: 14px;
color: #555555;
margin-top: 8px;
flex-wrap: wrap;
}
.item-box.item-project .introduce .line {
color: #d7d7d7;
margin: 0 8px;
}
.item-box.item-project .introduce .q {
font-family: "Arial", "Arial-Black", "Arial Black", sans-serif;
font-weight: 900;
color: #000000;
margin-left: 8px;
}
.item-box.item-project .word {
font-size: 14px;
color: #7f7f7f;
padding: 0 10px;
height: 46px;
line-height: 46px;
background-color: #f6f6f6;
border-radius: 10px;
margin-top: 10px;
word-break: break-all;
display: inherit;
}
.item-box.item-project .tag {
flex-wrap: wrap;
}
.item-box.item-project .tag .tag-item {
font-size: 14px;
color: #555555;
padding: 0 8px;
width: fit-content;
height: 24px;
line-height: 24px;
background-color: #f2f2f2;
border-radius: 6px;
margin-bottom: 10px;
}
.item-box.item-project .tag .tag-item.admissions {
background-color: #04b0d5;
border: none;
color: #fff;
}
.item-box.item-project .tag .tag-item.gray {
border: none;
color: #fff;
background-color: #333333;
}
.item-box.item-project .tag .tag-item.gray.semester {
background-color: #f95d5d;
}
.item-box.item-project .tag .tag-item:not(:last-child) {
margin-right: 10px;
}
.item-box.item-project .tag .tag-item.blue {
color: #ffffff;
background-color: #04b0d5;
}
.item-box.item-project .tag .tag-item.icon {
height: 24px;
width: 94px;
padding: 0;
margin-right: 10px;
}
.item-box .comment { .item-box .comment {
height: 40px; height: 40px;
background-color: #f6f6f6; background-color: #f6f6f6;
@@ -1123,7 +1220,7 @@ body {
margin-right: 10px; margin-right: 10px;
} }
.side-box.newest-side-box .box .item .dot.dot-green { .side-box.newest-side-box .box .item .dot.dot-green {
background-image: url(/img/dot-green.svg); background-image: url(../img/dot-green.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.side-box.newest-side-box .box .item .text { .side-box.newest-side-box .box .item .text {
@@ -1158,7 +1255,7 @@ body {
width: 6px; width: 6px;
height: 6px; height: 6px;
margin-right: 10px; margin-right: 10px;
background-image: url(/img/dot-blue.svg); background-image: url(../img/dot-blue.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.side-box.essence-side-box .box .item .text { .side-box.essence-side-box .box .item .text {

View File

@@ -728,6 +728,119 @@ body {
} }
} }
&.item-project {
padding-bottom: 18px;
.school {
.icon {
width: 18px;
height: 20px;
margin-right: 8px;
}
color: #333333;
font-size: 14px;
margin-top: 2px;
margin-bottom: 5px;
}
.name {
word-break: break-word;
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
font-weight: 650;
font-style: normal;
font-size: 20px;
color: #000000;
line-height: 36px;
}
.en {
font-size: 14px;
color: #7f7f7f;
margin-top: 4px;
word-break: break-all;
}
.introduce {
font-size: 14px;
color: #555555;
margin-top: 8px;
flex-wrap: wrap;
.line {
color: #d7d7d7;
margin: 0 8px;
}
.q {
font-family: "Arial", "Arial-Black", "Arial Black", sans-serif;
font-weight: 900;
color: #000000;
margin-left: 8px;
}
}
.word {
font-size: 14px;
color: #7f7f7f;
padding: 0 10px;
height: 46px;
line-height: 46px;
background-color: #f6f6f6;
border-radius: 10px;
margin-top: 10px;
word-break: break-all;
display: inherit;
}
.tag {
flex-wrap: wrap;
.tag-item {
&.admissions {
background-color: #04b0d5;
border: none;
color: #fff;
}
&.gray {
border: none;
color: #fff;
background-color: rgba(51, 51, 51, 1);
&.semester {
background-color: #f95d5d;
}
}
font-size: 14px;
color: #555555;
padding: 0 8px;
width: fit-content;
height: 24px;
line-height: 24px;
background-color: #f2f2f2;
border-radius: 6px;
margin-bottom: 10px;
&:not(:last-child) {
margin-right: 10px;
}
&.blue {
color: #ffffff;
background-color: #04b0d5;
}
&.icon {
height: 24px;
width: 94px;
padding: 0;
margin-right: 10px;
}
}
}
}
.comment { .comment {
height: 40px; height: 40px;
background-color: rgba(246, 246, 246, 1); background-color: rgba(246, 246, 246, 1);
@@ -1360,7 +1473,7 @@ body {
} }
.side-box.newest-side-box .box .item .dot.dot-green { .side-box.newest-side-box .box .item .dot.dot-green {
background-image: url(/img/dot-green.svg); background-image: url(../img/dot-green.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
} }
@@ -1403,7 +1516,7 @@ body {
width: 6px; width: 6px;
height: 6px; height: 6px;
margin-right: 10px; margin-right: 10px;
background-image: url(/img/dot-blue.svg); background-image: url(../img/dot-blue.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
} }
@@ -1831,7 +1944,6 @@ body {
} }
} }
.input { .input {
border: none; border: none;
outline: none; outline: none;
@@ -2160,4 +2272,4 @@ td {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }

View File

@@ -25,7 +25,8 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
#search .classify .item { #search .classify .item {
width: 50px; min-width: 50px;
padding: 0 8px;
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;

View File

@@ -27,7 +27,8 @@
.classify { .classify {
margin-bottom: 16px; margin-bottom: 16px;
.item { .item {
width: 50px; min-width: 50px;
padding: 0 8px;
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;

View File

@@ -29,6 +29,8 @@ const watchList = {
"../component/hot-tag/hot-tag.txt": "../component/hot-tag/hot-tag.js", "../component/hot-tag/hot-tag.txt": "../component/hot-tag/hot-tag.js",
// 监听 hot-search.txt同步到 hot-search.js // 监听 hot-search.txt同步到 hot-search.js
"../component/hot-search/hot-search.txt": "../component/hot-search/hot-search.js", "../component/hot-search/hot-search.txt": "../component/hot-search/hot-search.js",
// 监听 item-project.txt同步到 item-project.js
"../component/item-project/item-project.txt": "../component/item-project/item-project.js",
// 监听 bi.txt同步到 bi.js // 监听 bi.txt同步到 bi.js
"../component/bi/bi.txt": "../component/bi/bi.js", "../component/bi/bi.txt": "../component/bi/bi.js",

View File

@@ -1,16 +1,17 @@
const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue; const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue;
import { itemForum } from "/component/item-forum/item-forum.js"; import { itemForum } from "../component/item-forum/item-forum.js";
import { itemOffer } from "/component/item-offer/item-offer.js"; import { itemOffer } from "../component/item-offer/item-offer.js";
import { itemSummary } from "/component/item-summary/item-summary.js"; import { itemSummary } from "../component/item-summary/item-summary.js";
import { itemVote } from "/component/item-vote/item-vote.js"; import { itemVote } from "../component/item-vote/item-vote.js";
import { itemMj } from "/component/item-mj/item-mj.js"; import { itemMj } from "../component/item-mj/item-mj.js";
import { itemTenement } from "/component/item-tenement/item-tenement.js"; import { itemTenement } from "../component/item-tenement/item-tenement.js";
import { headTop } from "/component/head-top/head-top.js"; import { itemProject } from "../component/item-project/item-project.js";
import { hotTag } from "/component/hot-tag/hot-tag.js"; import { headTop } from "../component/head-top/head-top.js";
import { hotSearch } from "/component/hot-search/hot-search.js"; import { hotTag } from "../component/hot-tag/hot-tag.js";
import { slideshowBox } from "/component/slideshow-box/slideshow-box.js"; import { hotSearch } from "../component/hot-search/hot-search.js";
import { latestList } from "/component/latest-list/latest-list.js"; import { slideshowBox } from "../component/slideshow-box/slideshow-box.js";
import { loadBox } from "/component/load-box/load-box.js"; import { latestList } from "../component/latest-list/latest-list.js";
import { loadBox } from "../component/load-box/load-box.js";
const appSearch = createApp({ const appSearch = createApp({
setup() { setup() {
@@ -18,7 +19,8 @@ const appSearch = createApp({
let typeValue = ref(null); let typeValue = ref(null);
let kw = ref(""); let kw = ref("");
onMounted(() => { onMounted(() => {
// const params = getUrlParams(); const params = getUrlParams();
console.log("params", params);
// kw.value = params.kw || ""; // kw.value = params.kw || "";
// const urlObj = new URL(location.href); // const urlObj = new URL(location.href);
// const pathParts = urlObj.pathname.split("/").filter((part) => part); // const pathParts = urlObj.pathname.split("/").filter((part) => part);
@@ -26,9 +28,11 @@ const appSearch = createApp({
kw.value = kwValue.value.innerText; kw.value = kwValue.value.innerText;
const tab = typeValue.value.innerText; const tab = typeValue.value.innerText;
if (tab) tabValue.value = tab; if (tab) tabValue.value = tab;
if (params.page) page.value = params.page;
else page.value = 1;
page.value = 1; if (kw.value) getList();
getList(); else page.value = null;
getUserInfoWin(); getUserInfoWin();
@@ -125,6 +129,8 @@ const appSearch = createApp({
if (loading.value || page.value == null) return; if (loading.value || page.value == null) return;
loading.value = true; loading.value = true;
const limit = 20; const limit = 20;
window.scrollTo(0, 0);
updateUrlParams({ page: page.value });
ajaxGet(`/v2/api/forum/topicLists?type=${tabValue.value == "all" ? "" : tabValue.value}&page=${page.value}&limit=${limit}&keyword=${kw.value}`) ajaxGet(`/v2/api/forum/topicLists?type=${tabValue.value == "all" ? "" : tabValue.value}&page=${page.value}&limit=${limit}&keyword=${kw.value}`)
.then((res) => { .then((res) => {
if (res.code != 200) { if (res.code != 200) {
@@ -133,6 +139,38 @@ const appSearch = createApp({
} }
let data = res.data; let data = res.data;
data.data.unshift({
id: 20,
program_en: "Master of Laws in Arbitration and Dispute Resolution",
program_zh: "法学硕士(仲裁及争议解决学)",
program_abbr: "LLMARBDR",
program_code: "P41",
award_en: "Master of Laws in Arbitration and Dispute Resolution",
award_zh: "法学硕士(仲裁及争议解决学)",
subject_area_id: 9,
subject_area_name: "Law",
primary_university: "City University of Hong Kong",
primary_university_id: 3,
status: "ACTIVE",
intake_year: 2026,
disciplineid: 9,
distinctive: "毕业生可参与:当事人、辩护人、专家、仲裁员和调解员",
rank: "42",
department: "法律学院",
admissionsproject: "1",
departmentid: 26,
schoolalias: "城大",
schoolname: "香港城市大学",
tags: ["有奖学金", "论文课程", "26fall 提前批", "Top 50", "专业资格认证"],
schoolenname: "City University of Hong Kong",
intake_month: 9,
schoolid: 311,
tuition_fee: null,
uniqid: "tf1yFYMER8-1bY1t5oLbKaNc2FVhOWM0",
type: "programs",
schoollogo: "https://oss.x-php.com/school/J6BSwE-VfCFkCb1SBaR7ec6NYmTA4pRcOalNHJRfNzUxNg~~",
});
list.value = data.data; list.value = data.data;
if (list.value.length == 0) page.value = null; if (list.value.length == 0) page.value = null;
@@ -141,7 +179,14 @@ const appSearch = createApp({
maxPage.value = Math.ceil(count.value / limit); maxPage.value = Math.ceil(count.value / limit);
pagination.value = calculatePagination(page.value, maxPage.value); pagination.value = calculatePagination(page.value, maxPage.value);
updateUrlLastPath(kw.value); let url = `/search/${kw.value}`;
const hostname = location.hostname;
const localHostReg = /^(localhost|127\.0\.0\.1|\[::1\])$/;
if (localHostReg.test(hostname)) url = `/search.html`;
updateUrlLastPath(url);
removeQueryQ();
}) })
.catch((err) => { .catch((err) => {
err = err.data; err = err.data;
@@ -227,29 +272,13 @@ const appSearch = createApp({
const sidebarFixed = ref(false); const sidebarFixed = ref(false);
const matterFixed = ref(false); const matterFixed = ref(false);
const matterBottom = ref(false);
const handleScroll = () => { const handleScroll = () => {
matterHeight.value = -(matterContentRef.value.offsetHeight - window.innerHeight);
matterHeight.value = -(document.querySelector(".matter-content").offsetHeight - window.innerHeight); sidebarHeight.value = -(sidebarRef.value.offsetHeight - window.innerHeight);
sidebarHeight.value = -(document.querySelector(".sidebar-box").offsetHeight - window.innerHeight); if (matterHeight.value > 0) matterHeight.value = 12;
if (sidebarHeight.value > 0) sidebarHeight.value = 12;
// const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
// const clientHeight = window.innerHeight;
// const sideHeight = sidebarRef.value.offsetHeight;
// const matterTop = matterRef.value.offsetTop;
// const matterHeight = matterContentRef.value.offsetHeight;
// console.log("sideHeight", sideHeight);
// console.log("matterHeight", matterHeight);
// if (sideHeight < matterHeight) {
// // 侧边栏滚动固定
// if (scrollTop >= matterTop + sideHeight - clientHeight) sidebarFixed.value = true;
// else sidebarFixed.value = false;
// } else {
// if (scrollTop >= matterTop + matterHeight - clientHeight) matterFixed.value = true;
// else matterFixed.value = false;
// }
}; };
const matterRef = ref(null); const matterRef = ref(null);
@@ -259,7 +288,7 @@ const appSearch = createApp({
let sidebarHeight = ref(0); let sidebarHeight = ref(0);
let matterHeight = ref(0); let matterHeight = ref(0);
return { matterHeight, sidebarHeight, matterFixed, matterContentRef, sidebarFixed, matterRef, sidebarRef, loading, typeValue, kwValue, startSearch, kw, maxPage, prevPage, nextPage, tabValue, cutTab, tabList, count, list, page, pagination, cutPage }; return { matterHeight, sidebarHeight, matterBottom, matterFixed, matterContentRef, sidebarFixed, matterRef, sidebarRef, loading, typeValue, kwValue, startSearch, kw, maxPage, prevPage, nextPage, tabValue, cutTab, tabList, count, list, page, pagination, cutPage };
}, },
}); });
appSearch.component("item-forum", itemForum); appSearch.component("item-forum", itemForum);
@@ -268,6 +297,7 @@ appSearch.component("itemSummary", itemSummary);
appSearch.component("itemVote", itemVote); appSearch.component("itemVote", itemVote);
appSearch.component("itemMj", itemMj); appSearch.component("itemMj", itemMj);
appSearch.component("itemTenement", itemTenement); appSearch.component("itemTenement", itemTenement);
appSearch.component("itemProject", itemProject);
appSearch.component("head-top", headTop); appSearch.component("head-top", headTop);
appSearch.component("hot-tag", hotTag); appSearch.component("hot-tag", hotTag);
appSearch.component("hot-search", hotSearch); appSearch.component("hot-search", hotSearch);

View File

@@ -1,84 +1,89 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>搜索</title>
<link rel="stylesheet" href="./css/public.css" />
<link rel="stylesheet" href="./css/search.css" />
<script src="./js/vue.global.js"></script>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body> <head>
<div class="container" id="search" v-cloak> <meta charset="UTF-8" />
<div class="templateValue" ref="kwValue">香港</div> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<div class="templateValue" ref="typeValue"></div> <title>搜索</title>
<link rel="stylesheet" href="./css/public.css" />
<link rel="stylesheet" href="./css/search.css" />
<script src="./js/vue.global.js"></script>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<div class="head-top flexacenter"> <body>
<img class="logo" src="https://oss.gter.net/logo" alt="" /> <div class="container" id="search" v-cloak>
<div class="flex1"></div> <div class="templateValue" ref="kwValue">香港</div>
</div> <div class="templateValue" ref="typeValue"></div>
<div class="search-box flexacenter">
<input class="search-input flex1" placeholder="请输入搜索关键词" v-model="kw" @keyup.enter="startSearch" />
<img class="search-icon" src="./img/search-icon.svg" alt="" @click="startSearch" />
</div>
<div class="classify flexacenter"> <div class="head-top flexacenter">
<div class="item" :class="{'pitch': key == tabValue}" v-for="(item, key) in tabList" :key="key" @click="cutTab(key)">{{ item }}</div> <img class="logo" src="https://oss.gter.net/logo" alt="" />
</div> <div class="flex1"></div>
</div>
<div class="quantity flexacenter"> <div class="search-box flexacenter">
{{ tabList[tabValue] }} <input class="search-input flex1" placeholder="请输入搜索关键词" v-model="kw" @keyup.enter="startSearch" />
<div class="line"></div> <img class="search-icon" src="./img/search-icon.svg" alt="" @click="startSearch" />
<div class="num">{{ count }}</div>
</div>
<div class="matter flexflex">
<div class="matter-content flex1" :style="{'top': matterHeight + 'px'}">
<div class="list-box" v-if="list.length != 0">
<template v-for="(item,index) in list" :key="index">
<item-offer v-if=" item.type == 'offer'" :itemdata="item"></item-offer>
<item-summary v-else-if="item.type == 'offer_summary'" :itemdata="item"></item-summary>
<item-vote v-else-if="item.type == 'vote'" :itemdata="item"></item-vote>
<item-mj v-else-if="item.type == 'interviewexperience'" :itemdata="item"></item-mj>
<item-tenement v-else-if="item.type == 'tenement'" :itemdata="item"></item-tenement>
<item-forum v-else :itemdata="item"></item-forum>
</template>
</div>
<div v-if="list.length == 0 && page == null" class="empty flexcenter">
<img class="empty-icon" src="./img/empty-icon.png" />
<div class="empty-text">- 暂无内容 -</div>
</div>
<div class="pages-box flexcenter" v-if="pagination.length != 0">
<img v-if="page == 1" class="arrows" src="./img/arrows-gray-simple.svg" alt="" />
<img @click="prevPage" v-else class="arrows rotate180" src="./img/arrows-gray-deep.svg" alt="" />
<div class="item" :class="{'pitch': item == page }" v-for="(item, index) in pagination" @click="cutPage(item)">{{ item }}</div>
<img v-if="page == maxPage" class="arrows rotate180" src="./img/arrows-gray-simple.svg" alt="" />
<img @click="nextPage" v-else v-else class="arrows" src="./img/arrows-gray-deep.svg" alt="" />
</div>
</div>
<div class="sidebar-box" :style="{'top': sidebarHeight + 'px'}">
<hot-search></hot-search>
<hot-tag></hot-tag>
<slideshow-box></slideshow-box>
<latest-list></latest-list>
</div>
</div>
</div> </div>
<script src="./js/axios.min.js"></script> <div class="classify flexacenter">
<script src="./js/public.js"></script> <div class="item" :class="{'pitch': key == tabValue}" v-for="(item, key) in tabList" :key="key"
<script type="module" src="./js/search.js"></script> @click="cutTab(key)">{{ item }}</div>
</body> </div>
</html>
<div class="quantity flexacenter">
{{ tabList[tabValue] }}
<div class="line"></div>
<div class="num">{{ count }}</div>
</div>
<div class="matter flexflex">
<div class="matter-content flex1" :style="{'top': matterHeight + 'px'}">
<div class="list-box" v-if="list.length != 0">
<template v-for="(item,index) in list" :key="index">
<item-project v-if="item.type == 'programs'" :itemdata="item"></item-project>
<item-offer v-else-if="item.type == 'offer'" :itemdata="item"></item-offer>
<item-summary v-else-if="item.type == 'offer_summary'" :itemdata="item"></item-summary>
<item-vote v-else-if="item.type == 'vote'" :itemdata="item"></item-vote>
<item-mj v-else-if="item.type == 'interviewexperience'" :itemdata="item"></item-mj>
<item-tenement v-else-if="item.type == 'tenement'" :itemdata="item"></item-tenement>
<item-forum v-else :itemdata="item"></item-forum>
</template>
</div>
<div v-if="list.length == 0 && page == null" class="empty flexcenter">
<img class="empty-icon" src="./img/empty-icon.png" />
<div class="empty-text">- 暂无内容 -</div>
</div>
<div class="pages-box flexcenter" v-if="pagination.length != 0">
<img v-if="page == 1" class="arrows" src="./img/arrows-gray-simple.svg" alt="" />
<img @click="prevPage" v-else class="arrows rotate180" src="./img/arrows-gray-deep.svg" alt="" />
<div class="item" :class="{'pitch': item == page }" v-for="(item, index) in pagination"
@click="cutPage(item)">{{ item }}</div>
<img v-if="page == maxPage" class="arrows rotate180" src="./img/arrows-gray-simple.svg" alt="" />
<img @click="nextPage" v-else v-else class="arrows" src="./img/arrows-gray-deep.svg" alt="" />
</div>
</div>
<div class="sidebar-box" :style="{'top': sidebarHeight + 'px'}">
<hot-search></hot-search>
<hot-tag></hot-tag>
<slideshow-box></slideshow-box>
<latest-list></latest-list>
</div>
</div>
</div>
<script src="./js/axios.min.js"></script>
<script src="./js/public.js"></script>
<script type="module" src="./js/search.js"></script>
</body>
</html>

56
serve.ps1 Normal file
View File

@@ -0,0 +1,56 @@
$base = Split-Path -Parent $MyInvocation.MyCommand.Path
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 5501)
$listener.Start()
Write-Host "http://127.0.0.1:5501/"
while ($true) {
$client = $listener.AcceptTcpClient()
try {
$stream = $client.GetStream()
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 1024, $true)
$writer = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true
$requestLine = $reader.ReadLine()
if (-not $requestLine) { $client.Close(); continue }
while ($true) { $line = $reader.ReadLine(); if ($line -eq $null -or $line -eq "") { break } }
$rawPath = "/"
if ($requestLine -match "^GET\s+([^\s]+)") { $rawPath = $matches[1] }
$pathOnly = $rawPath.Split("?")[0]
$pathOnly = [System.Uri]::UnescapeDataString($pathOnly)
$reqExt = [System.IO.Path]::GetExtension($pathOnly)
if ($pathOnly -like "/search/*" -and [string]::IsNullOrEmpty($reqExt)) { $file = Join-Path $base "search.html" }
elseif ($pathOnly -eq "/") { $file = Join-Path $base "index.html" }
else { $rel = $pathOnly.TrimStart("/"); $file = Join-Path $base $rel }
if (-not (Test-Path $file)) {
$status = "404 Not Found"
$body = [System.Text.Encoding]::UTF8.GetBytes("Not Found")
$contentType = "text/plain; charset=utf-8"
}
else {
$status = "200 OK"
$body = [System.IO.File]::ReadAllBytes($file)
$ext = [System.IO.Path]::GetExtension($file).ToLower()
switch ($ext) {
".html" { $contentType = "text/html; charset=utf-8" }
".css" { $contentType = "text/css" }
".js" { $contentType = "application/javascript" }
".png" { $contentType = "image/png" }
".jpg" { $contentType = "image/jpeg" }
".jpeg" { $contentType = "image/jpeg" }
".svg" { $contentType = "image/svg+xml" }
".json" { $contentType = "application/json" }
".ico" { $contentType = "image/x-icon" }
default { $contentType = "application/octet-stream" }
}
}
$writer.WriteLine("HTTP/1.1 $status")
$writer.WriteLine("Content-Type: $contentType")
$writer.WriteLine("Content-Length: " + $body.Length)
$writer.WriteLine("Connection: close")
$writer.WriteLine("")
$stream.Write($body, 0, $body.Length)
} catch {
try { $client.Close() } catch {}
} finally {
try { $client.Close() } catch {}
}
}