feat: add service order component

- add CreateServiceOrder component and associated icon assets
- create reusable resolveChatSocketUrl utility with comprehensive test cases
- update development env config to use production websocket endpoint
- fix ChatCardAi layout by replacing inline-block with flex-1 class
- refactor ChatMainList websocket initialization to use the new socket utility
- switch to using environment variable for access token instead of getAccessToken
- correct relative import path for CreateServiceOrder in ChatMainList
This commit is contained in:
DEV_DSW
2026-06-01 11:44:56 +08:00
parent 861ebf4fd7
commit b906058cdc
8 changed files with 99 additions and 20 deletions

View File

@@ -8,7 +8,7 @@ VITE_API_BASE_URL = '/ingress'
VITE_API_TIMEOUT_MS = 10000
# Socket 基础 URL
VITE_SOCKET_BASE_URL = "/ingress/agent/ws/chat"
VITE_SOCKET_BASE_URL = "wss://onefeel.brother7.cn/ingress/agent/ws/chat"
# Client ID
VITE_CLIENT_ID = "6"

View File

@@ -1,7 +1,7 @@
<template>
<!-- 外层行容器接收来自父组件的对齐类 flex flex-justify-start并占满宽度 -->
<div class="w-full flex items-center justify-start">
<div class="inline-block align-middle max-w-full overflow-x-hidden">
<div class="align-middle flex-1 overflow-x-hidden">
<div class="px-3 min-w-0 max-w-full overflow-hidden break-all">
<div class="flex items-center max-w-full">
<img v-if="isLoading" class="mr-2 w-7.5 h-6.25" src="https://oss.nianxx.cn/mp/static/chat_msg_loading.gif" />

View File

@@ -168,13 +168,14 @@ import OpenMapComponent from "../OpenMapComponent/index.vue";
import AnswerComponent from "../AnswerComponent/index.vue";
import GeneratorPhotoComponent from "../GeneratorPhotoComponent/index.vue";
import LongTextGuideCardPreview from "../LongTextGuideCardPreview/index.vue";
import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue";
import CreateServiceOrder from "../CreateServiceOrder/index.vue";
import Feedback from "@/components/Feedback/index.vue";
import AddCarCrad from "@/components/AddCarCrad/index.vue";
import SurveyQuestionnaire from "@/components/SurveyQuestionnaire/index.vue";
import { mainPageData, conversationMsgList, recentConversation } from "@/api/home";
import WebSocketManager from "@/utils/WebSocketManager";
import { IdUtils } from "@/utils/IdUtils";
import { resolveChatSocketUrl } from "@/utils/socketUrl";
import { appendLongTextChunk, createLongTextData } from "@/constants/longTextCard";
import { checkToken } from "@/hooks/useGoLogin";
import { useAppStore } from "@/store";
@@ -543,18 +544,9 @@ const initWebSocket = async () => {
}
// 使用配置的WebSocket服务器地址
const token = getAccessToken();
const rawWsUrl = appStore.serverConfig.wssUrl;
const resolvedWsUrl =
typeof rawWsUrl === "string" &&
(rawWsUrl.startsWith("ws://") || rawWsUrl.startsWith("wss://"))
? rawWsUrl
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}${typeof rawWsUrl === "string"
? rawWsUrl.startsWith("/")
? rawWsUrl
: `/${rawWsUrl}`
: ""
}`;
// const token = getAccessToken();
const token = import.meta.env.VITE_TOKEN;
const resolvedWsUrl = resolveChatSocketUrl(appStore.serverConfig.wssUrl);
const wsUrl = `${resolvedWsUrl}?access_token=${token}`;
// 初始化WebSocket管理器

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 844 B

View File

@@ -51,15 +51,14 @@
联系方式: {{ contactPhone }}
</div>
<div class="text-[12px] text-ink-600 leading-[20px] ellipsis-2">
需求描述: {{ contactspan }}
需求描述: {{ contactText }}
</div>
</div>
<img v-if="contentImgUrl" class="w-[88px] h-[88px] rounded-[6px]" :src="contentImgUrl" mode="aspectFill" />
<img v-if="contentImgUrl" class="w-[88px] h-[88px] rounded-[6px]" :src="contentImgUrl" />
</div>
<div
class="h-[44px] rounded-[5px] text-white bg-linear-[90deg,#f0f8f3_0%,#4de4ff_100%] flex items-center justify-center mx-[12px] mb-[12px]"
<div class="h-[44px] rounded-full text-white bg-[#4de4ff] flex items-center justify-center mx-[12px] mb-[12px]"
@click="handleCall">
{{ isCallSuccess ? "查看服务" : "立即呼叫" }}
</div>
@@ -100,7 +99,7 @@ const contactPhone = ref("");
//
const hasEditedPhone = ref(false);
// 使 ref
const contactspan = ref("");
const contactText = ref("");
// 138****123411
const maskPhone = (phone) => {

View File

@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { resolveChatSocketUrl } from "./socketUrl.ts";
const locationLike = {
protocol: "http:",
host: "localhost:5174",
};
describe("resolveChatSocketUrl", () => {
it("keeps a complete ws chat endpoint", () => {
assert.equal(
resolveChatSocketUrl(
"wss://onefeel.brother7.cn/ingress/agent/ws/chat",
locationLike,
),
"wss://onefeel.brother7.cn/ingress/agent/ws/chat",
);
});
it("adds the chat path to a configured websocket origin", () => {
assert.equal(
resolveChatSocketUrl("wss://onefeel.brother7.cn", locationLike),
"wss://onefeel.brother7.cn/ingress/agent/ws/chat",
);
});
it("adds the chat path to an ingress base path", () => {
assert.equal(
resolveChatSocketUrl("/ingress", locationLike),
"ws://localhost:5174/ingress/agent/ws/chat",
);
});
it("uses the current host when config is empty", () => {
assert.equal(
resolveChatSocketUrl("", locationLike),
"ws://localhost:5174/ingress/agent/ws/chat",
);
});
});

47
src/utils/socketUrl.ts Normal file
View File

@@ -0,0 +1,47 @@
const CHAT_SOCKET_PATH = "/ingress/agent/ws/chat";
type LocationLike = {
protocol: string;
host: string;
};
const toWsProtocol = (protocol: string) => {
if (protocol === "https:" || protocol === "wss:") return "wss:";
return "ws:";
};
const resolveChatPath = (pathname: string) => {
const normalizedPath = pathname === "/" ? "" : pathname.replace(/\/$/, "");
if (!normalizedPath) return CHAT_SOCKET_PATH;
if (normalizedPath === CHAT_SOCKET_PATH) return CHAT_SOCKET_PATH;
if (CHAT_SOCKET_PATH.startsWith(`${normalizedPath}/`)) return CHAT_SOCKET_PATH;
return `${normalizedPath}${CHAT_SOCKET_PATH}`;
};
export const resolveChatSocketUrl = (
rawSocketBaseUrl: unknown,
currentLocation: LocationLike = window.location,
) => {
const socketBaseUrl =
typeof rawSocketBaseUrl === "string" ? rawSocketBaseUrl.trim() : "";
const fallbackProtocol = toWsProtocol(currentLocation.protocol);
if (!socketBaseUrl) {
return `${fallbackProtocol}//${currentLocation.host}${CHAT_SOCKET_PATH}`;
}
if (/^(https?|wss?):\/\//.test(socketBaseUrl)) {
const url = new URL(socketBaseUrl);
url.protocol = toWsProtocol(url.protocol);
url.pathname = resolveChatPath(url.pathname);
return url.toString().replace(/\/$/, "");
}
const normalizedPath = socketBaseUrl.startsWith("/")
? socketBaseUrl
: `/${socketBaseUrl}`;
return `${fallbackProtocol}//${currentLocation.host}${resolveChatPath(normalizedPath)}`;
};