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-bottom: 38px;
margin-right: 20px;
position: sticky;
top: 10px;
}
#homepage-me .matter .card-user .avatar-box {
position: relative;

View File

@@ -16,6 +16,9 @@
padding-top: 39px;
padding-bottom: 38px;
margin-right: 20px;
position: sticky;
top: 10px;
.avatar-box {
position: relative;
margin-bottom: 20px;

View File

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

View File

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

View File

@@ -600,6 +600,103 @@ body {
.item-box.item-tenement .picture .picture-item:not(:last-child) {
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 {
height: 40px;
background-color: #f6f6f6;
@@ -1123,7 +1220,7 @@ body {
margin-right: 10px;
}
.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;
}
.side-box.newest-side-box .box .item .text {
@@ -1158,7 +1255,7 @@ body {
width: 6px;
height: 6px;
margin-right: 10px;
background-image: url(/img/dot-blue.svg);
background-image: url(../img/dot-blue.svg);
background-repeat: no-repeat;
}
.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 {
height: 40px;
background-color: rgba(246, 246, 246, 1);
@@ -1360,7 +1473,7 @@ body {
}
.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;
}
@@ -1403,7 +1516,7 @@ body {
width: 6px;
height: 6px;
margin-right: 10px;
background-image: url(/img/dot-blue.svg);
background-image: url(../img/dot-blue.svg);
background-repeat: no-repeat;
}
@@ -1831,7 +1944,6 @@ body {
}
}
.input {
border: none;
outline: none;

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ const watchList = {
"../component/hot-tag/hot-tag.txt": "../component/hot-tag/hot-tag.js",
// 监听 hot-search.txt同步到 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
"../component/bi/bi.txt": "../component/bi/bi.js",

View File

@@ -1,16 +1,17 @@
const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue;
import { itemForum } from "/component/item-forum/item-forum.js";
import { itemOffer } from "/component/item-offer/item-offer.js";
import { itemSummary } from "/component/item-summary/item-summary.js";
import { itemVote } from "/component/item-vote/item-vote.js";
import { itemMj } from "/component/item-mj/item-mj.js";
import { itemTenement } from "/component/item-tenement/item-tenement.js";
import { headTop } from "/component/head-top/head-top.js";
import { hotTag } from "/component/hot-tag/hot-tag.js";
import { hotSearch } from "/component/hot-search/hot-search.js";
import { slideshowBox } from "/component/slideshow-box/slideshow-box.js";
import { latestList } from "/component/latest-list/latest-list.js";
import { loadBox } from "/component/load-box/load-box.js";
import { itemForum } from "../component/item-forum/item-forum.js";
import { itemOffer } from "../component/item-offer/item-offer.js";
import { itemSummary } from "../component/item-summary/item-summary.js";
import { itemVote } from "../component/item-vote/item-vote.js";
import { itemMj } from "../component/item-mj/item-mj.js";
import { itemTenement } from "../component/item-tenement/item-tenement.js";
import { itemProject } from "../component/item-project/item-project.js";
import { headTop } from "../component/head-top/head-top.js";
import { hotTag } from "../component/hot-tag/hot-tag.js";
import { hotSearch } from "../component/hot-search/hot-search.js";
import { slideshowBox } from "../component/slideshow-box/slideshow-box.js";
import { latestList } from "../component/latest-list/latest-list.js";
import { loadBox } from "../component/load-box/load-box.js";
const appSearch = createApp({
setup() {
@@ -18,7 +19,8 @@ const appSearch = createApp({
let typeValue = ref(null);
let kw = ref("");
onMounted(() => {
// const params = getUrlParams();
const params = getUrlParams();
console.log("params", params);
// kw.value = params.kw || "";
// const urlObj = new URL(location.href);
// const pathParts = urlObj.pathname.split("/").filter((part) => part);
@@ -26,9 +28,11 @@ const appSearch = createApp({
kw.value = kwValue.value.innerText;
const tab = typeValue.value.innerText;
if (tab) tabValue.value = tab;
if (params.page) page.value = params.page;
else page.value = 1;
page.value = 1;
getList();
if (kw.value) getList();
else page.value = null;
getUserInfoWin();
@@ -125,6 +129,8 @@ const appSearch = createApp({
if (loading.value || page.value == null) return;
loading.value = true;
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}`)
.then((res) => {
if (res.code != 200) {
@@ -133,6 +139,38 @@ const appSearch = createApp({
}
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;
if (list.value.length == 0) page.value = null;
@@ -141,7 +179,14 @@ const appSearch = createApp({
maxPage.value = Math.ceil(count.value / limit);
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) => {
err = err.data;
@@ -227,29 +272,13 @@ const appSearch = createApp({
const sidebarFixed = ref(false);
const matterFixed = ref(false);
const matterBottom = ref(false);
const handleScroll = () => {
matterHeight.value = -(document.querySelector(".matter-content").offsetHeight - window.innerHeight);
sidebarHeight.value = -(document.querySelector(".sidebar-box").offsetHeight - window.innerHeight);
// 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;
// }
matterHeight.value = -(matterContentRef.value.offsetHeight - window.innerHeight);
sidebarHeight.value = -(sidebarRef.value.offsetHeight - window.innerHeight);
if (matterHeight.value > 0) matterHeight.value = 12;
if (sidebarHeight.value > 0) sidebarHeight.value = 12;
};
const matterRef = ref(null);
@@ -259,7 +288,7 @@ const appSearch = createApp({
let sidebarHeight = 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);
@@ -268,6 +297,7 @@ appSearch.component("itemSummary", itemSummary);
appSearch.component("itemVote", itemVote);
appSearch.component("itemMj", itemMj);
appSearch.component("itemTenement", itemTenement);
appSearch.component("itemProject", itemProject);
appSearch.component("head-top", headTop);
appSearch.component("hot-tag", hotTag);
appSearch.component("hot-search", hotSearch);

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>搜索</title>
@@ -12,9 +13,9 @@
display: none;
}
</style>
</head>
</head>
<body>
<body>
<div class="container" id="search" v-cloak>
<div class="templateValue" ref="kwValue">香港</div>
<div class="templateValue" ref="typeValue"></div>
@@ -29,7 +30,8 @@
</div>
<div class="classify flexacenter">
<div class="item" :class="{'pitch': key == tabValue}" v-for="(item, key) in tabList" :key="key" @click="cutTab(key)">{{ item }}</div>
<div class="item" :class="{'pitch': key == tabValue}" v-for="(item, key) in tabList" :key="key"
@click="cutTab(key)">{{ item }}</div>
</div>
<div class="quantity flexacenter">
@@ -44,7 +46,8 @@
<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-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>
@@ -62,7 +65,8 @@
<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>
<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="" />
@@ -80,5 +84,6 @@
<script src="./js/axios.min.js"></script>
<script src="./js/public.js"></script>
<script type="module" src="./js/search.js"></script>
</body>
</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 {}
}
}