更新
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Doctor Video Companion
|
||||
|
||||
这是桌面端专用的最小 Vue 3 / Vite 视频页面。它与 admin 的独立视频组件保持同一条 SDK 主链,直接使用固定版本 `@trtc/calls-uikit-vue@4.4.6`。
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
cd video_companion
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建产物位于 `video_companion/dist/`。Vite 使用相对资源路径,因此产物既能由 HTTPS 托管,也能由 QtWebEngine 从本地安装目录加载。
|
||||
|
||||
桌面端当前只在隔离的 QtWebEngine 中加载此页面。页面不会自行向后端领取通话票据,因此在后端提供服务端签发的一次性 handoff 前,系统浏览器模式与自动浏览器降级均被禁用。
|
||||
|
||||
## 宿主契约
|
||||
|
||||
页面加载后会暴露:
|
||||
|
||||
```ts
|
||||
window.doctorCall.start({
|
||||
SDKAppID: 1400000000, // 也接受 sdkAppId
|
||||
userID: 'doctor_42', // 也接受 userId
|
||||
userSig: '<后端短时票据>',
|
||||
targetUserId: 'patient_8', // 也接受 patientUserId
|
||||
diagnosisId: 123,
|
||||
})
|
||||
|
||||
await window.doctorCall.hangup()
|
||||
```
|
||||
|
||||
`userSig` 必须由业务后端签发。此页面不会生成 UserSig,也不接受或使用 SDKSecretKey。
|
||||
|
||||
状态、错误与挂断消息优先调用 `window.qtVideoBridge.notify(JSON.stringify(message))`。没有 Qt bridge 时,页面使用 `postMessage` 通知父窗口或 opener;都不可用时只输出不含票据的控制台状态。
|
||||
@@ -0,0 +1 @@
|
||||
:root{font-family:Inter,PingFang SC,Microsoft YaHei,system-ui,sans-serif;color:#f7f8fa;background:#0b0f14;font-synthesis:none;text-rendering:optimizeLegibility}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0;overflow:hidden}button,input{font:inherit}.call-stage{position:relative;width:100%;height:100%;min-height:420px;overflow:hidden;background:radial-gradient(circle at 50% 35%,rgba(39,74,83,.22),transparent 38%),#0b0f14}.call-kit,.call-stage :is(.TUICallKit-desktop,.TUICallKit-mobile,#tuicallkit-id){width:100%!important;height:100%!important;max-width:none!important;max-height:none!important}.status-card{position:absolute;inset:50% auto auto 50%;display:grid;grid-template-columns:12px minmax(0,1fr);gap:18px;width:min(520px,calc(100% - 48px));padding:30px 32px;transform:translate(-50%,-50%);border:1px solid rgba(255,255,255,.09);border-radius:20px;background:#131920e6;box-shadow:0 24px 70px #00000052;backdrop-filter:blur(18px)}.eyebrow{margin:0 0 12px;color:#8f9ba8;font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase}.status-card h1{margin:0;font-size:clamp(22px,3.2vw,34px);font-weight:600;line-height:1.25}.status-hint{margin:14px 0 0;color:#9aa5b1;font-size:14px}.status-dot{width:10px;height:10px;margin-top:5px;border-radius:50%;background:#77818c;box-shadow:0 0 0 5px #77818c1f}.status-dot--starting,.status-dot--live{background:#52c99a;box-shadow:0 0 0 5px #52c99a24}.status-dot--error{background:#f26d6d;box-shadow:0 0 0 5px #f26d6d24}.live-status{position:absolute;z-index:20;top:18px;left:50%;display:flex;align-items:center;gap:10px;padding:9px 14px;transform:translate(-50%);border:1px solid rgba(255,255,255,.1);border-radius:999px;background:#0b0f14c2;color:#e8edf2;font-size:13px;backdrop-filter:blur(14px)}.live-status .status-dot{width:7px;height:7px;margin:0;box-shadow:none}
|
||||
+532
File diff suppressed because one or more lines are too long
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>视频面诊</title>
|
||||
<script type="module" crossorigin src="./assets/index-le5ZH3pL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BuO_uDya.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- QWebEngine 提供此资源;普通浏览器中加载失败不影响页面运行。 -->
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>视频面诊</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- QWebEngine 提供此资源;普通浏览器中加载失败不影响页面运行。 -->
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1604
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "doctor-video-companion",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trtc/calls-uikit-vue": "4.4.6",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.1.1",
|
||||
"vue-tsc": "2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { TUICallKit } from '@trtc/calls-uikit-vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
phase: Readonly<Ref<string>>
|
||||
statusText: Readonly<Ref<string>>
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="call-stage">
|
||||
<TUICallKit
|
||||
class="call-kit"
|
||||
:allowed-minimized="false"
|
||||
:allowed-full-screen="true"
|
||||
/>
|
||||
|
||||
<section v-if="phase.value === 'ready' || phase.value === 'starting' || phase.value === 'error'" class="status-card">
|
||||
<span class="status-dot" :class="`status-dot--${phase.value}`" aria-hidden="true" />
|
||||
<div>
|
||||
<p class="eyebrow">中医视频面诊</p>
|
||||
<h1>{{ statusText.value }}</h1>
|
||||
<p v-if="phase.value === 'ready'" class="status-hint">通话凭证仅由业务后端签发</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-else class="live-status" role="status">
|
||||
<span class="status-dot status-dot--live" aria-hidden="true" />
|
||||
{{ statusText.value }}
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface DoctorCallConfig {
|
||||
SDKAppID?: number | string
|
||||
sdkAppId?: number | string
|
||||
userID?: string
|
||||
userId?: string
|
||||
userSig: string
|
||||
targetUserId?: string
|
||||
patientUserId?: string
|
||||
diagnosisId: number | string
|
||||
}
|
||||
|
||||
interface DoctorCallApi {
|
||||
start(config: DoctorCallConfig): Promise<void>
|
||||
hangup(): Promise<void>
|
||||
}
|
||||
|
||||
interface QtVideoBridge {
|
||||
notify?: (payload: string) => void
|
||||
}
|
||||
|
||||
interface Window {
|
||||
doctorCall: DoctorCallApi
|
||||
qtVideoBridge?: QtVideoBridge
|
||||
qt?: { webChannelTransport?: unknown }
|
||||
QWebChannel?: new (
|
||||
transport: unknown,
|
||||
callback: (channel: { objects: { qtVideoBridge?: QtVideoBridge } }) => void,
|
||||
) => unknown
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { createApp, nextTick, readonly, ref } from 'vue'
|
||||
import {
|
||||
NAME,
|
||||
StoreName,
|
||||
TUIStore,
|
||||
TUICallKitAPI,
|
||||
TUICallType,
|
||||
} from '@trtc/calls-uikit-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
type CallPhase = 'ready' | 'starting' | 'dialing' | 'connected' | 'ended' | 'error'
|
||||
|
||||
interface NormalizedCallConfig {
|
||||
SDKAppID: number
|
||||
userID: string
|
||||
userSig: string
|
||||
targetUserId: string
|
||||
diagnosisId: number | string
|
||||
}
|
||||
|
||||
interface BridgeMessage {
|
||||
source: 'doctor-call'
|
||||
event: 'ready' | 'status' | 'room' | 'hangup' | 'error'
|
||||
diagnosisId?: number | string
|
||||
status?: string
|
||||
roomId?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const phase = ref<CallPhase>('ready')
|
||||
const statusText = ref('等待桌面端发起视频面诊')
|
||||
let activeConfig: NormalizedCallConfig | null = null
|
||||
let endNotified = false
|
||||
let starting = false
|
||||
let emittedRoomId = ''
|
||||
|
||||
function initializeQtWebChannel(): void {
|
||||
const transport = window.qt?.webChannelTransport
|
||||
const QWebChannel = window.QWebChannel
|
||||
if (!transport || typeof QWebChannel !== 'function') return
|
||||
|
||||
try {
|
||||
new QWebChannel(transport, (channel) => {
|
||||
const bridge = channel.objects.qtVideoBridge
|
||||
if (bridge) window.qtVideoBridge = bridge
|
||||
emit({ source: 'doctor-call', event: 'ready' })
|
||||
})
|
||||
} catch {
|
||||
console.warn('[doctor-call] Qt WebChannel 初始化失败,将使用 postMessage 通知')
|
||||
}
|
||||
}
|
||||
|
||||
function postToHost(message: BridgeMessage): boolean {
|
||||
let delivered = false
|
||||
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage(message, '*')
|
||||
delivered = true
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin host may reject access; opener remains available as a fallback.
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage(message, '*')
|
||||
delivered = true
|
||||
}
|
||||
} catch {
|
||||
// The console fallback below is intentionally non-sensitive.
|
||||
}
|
||||
|
||||
return delivered
|
||||
}
|
||||
|
||||
function emit(message: BridgeMessage): void {
|
||||
const bridge = window.qtVideoBridge
|
||||
if (bridge && typeof bridge.notify === 'function') {
|
||||
try {
|
||||
bridge.notify(JSON.stringify(message))
|
||||
return
|
||||
} catch {
|
||||
// A detached WebChannel object is equivalent to an unavailable bridge.
|
||||
}
|
||||
}
|
||||
|
||||
if (!postToHost(message)) {
|
||||
console.info('[doctor-call]', message.event, message.status ?? message.message ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
function cleanString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new Error(`${field} 不能为空`)
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function normalizeConfig(config: DoctorCallConfig): NormalizedCallConfig {
|
||||
if (!config || typeof config !== 'object') throw new Error('通话配置无效')
|
||||
|
||||
const rawSdkAppId = config.SDKAppID ?? config.sdkAppId
|
||||
const SDKAppID = Number(rawSdkAppId)
|
||||
if (!Number.isSafeInteger(SDKAppID) || SDKAppID <= 0) {
|
||||
throw new Error('SDKAppID 必须是正整数')
|
||||
}
|
||||
|
||||
const diagnosisId = config.diagnosisId
|
||||
if (
|
||||
diagnosisId === undefined
|
||||
|| diagnosisId === null
|
||||
|| (typeof diagnosisId === 'string' && diagnosisId.trim() === '')
|
||||
) {
|
||||
throw new Error('diagnosisId 不能为空')
|
||||
}
|
||||
|
||||
return {
|
||||
SDKAppID,
|
||||
userID: cleanString(config.userID ?? config.userId, 'userID'),
|
||||
userSig: cleanString(config.userSig, 'userSig'),
|
||||
targetUserId: cleanString(config.targetUserId ?? config.patientUserId, 'targetUserId'),
|
||||
diagnosisId: typeof diagnosisId === 'string' ? diagnosisId.trim() : diagnosisId,
|
||||
}
|
||||
}
|
||||
|
||||
function safeErrorMessage(error: unknown): string {
|
||||
let message = error instanceof Error ? error.message : '视频通话发生未知错误'
|
||||
if (activeConfig?.userSig) message = message.split(activeConfig.userSig).join('[REDACTED]')
|
||||
return message
|
||||
.replace(/(user\s*sig\s*[:=]\s*)[^\s,;&]+/gi, '$1[REDACTED]')
|
||||
.slice(0, 400)
|
||||
}
|
||||
|
||||
function notifyHangup(status = 'ended'): void {
|
||||
if (endNotified) return
|
||||
endNotified = true
|
||||
phase.value = 'ended'
|
||||
statusText.value = '视频面诊已结束'
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'hangup',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
function readRoomId(): string {
|
||||
const raw = TUIStore.getData(StoreName.CALL, NAME.ROOM_ID)
|
||||
if (raw === undefined || raw === null) return ''
|
||||
const value = String(raw).trim()
|
||||
return value && value !== '0' ? value : ''
|
||||
}
|
||||
|
||||
function emitRoomId(): boolean {
|
||||
const roomId = readRoomId()
|
||||
if (!roomId || roomId === emittedRoomId) return Boolean(roomId)
|
||||
emittedRoomId = roomId
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'room',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
roomId,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function pollRoomId(): Promise<void> {
|
||||
for (let attempt = 0; attempt < 40 && activeConfig && !endNotified; attempt += 1) {
|
||||
if (emitRoomId()) return
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 50))
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusChanged(payload: unknown): void {
|
||||
const value = payload && typeof payload === 'object'
|
||||
? (payload as { newStatus?: unknown }).newStatus
|
||||
: payload
|
||||
const status = typeof value === 'string' ? value : 'unknown'
|
||||
|
||||
if (status === 'connected' || status.startsWith('calling-')) {
|
||||
phase.value = 'connected'
|
||||
statusText.value = '视频面诊进行中'
|
||||
void pollRoomId()
|
||||
} else if (status === 'calling' || status.startsWith('dialing')) {
|
||||
phase.value = 'dialing'
|
||||
statusText.value = '正在等待患者接听'
|
||||
} else if (status === 'idle' && activeConfig && !starting) {
|
||||
notifyHangup(status)
|
||||
}
|
||||
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'status',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
TUICallKitAPI.setCallback({
|
||||
statusChanged: handleStatusChanged,
|
||||
afterCalling: () => notifyHangup('after-calling'),
|
||||
})
|
||||
TUICallKitAPI.enableFloatWindow(false)
|
||||
|
||||
async function start(config: DoctorCallConfig): Promise<void> {
|
||||
if (starting || (activeConfig && !endNotified)) {
|
||||
throw new Error('已有视频通话正在进行')
|
||||
}
|
||||
|
||||
starting = true
|
||||
endNotified = false
|
||||
phase.value = 'starting'
|
||||
statusText.value = '正在初始化安全通话'
|
||||
|
||||
try {
|
||||
const normalized = normalizeConfig(config)
|
||||
activeConfig = normalized
|
||||
emittedRoomId = ''
|
||||
|
||||
await TUICallKitAPI.init({
|
||||
SDKAppID: normalized.SDKAppID,
|
||||
userID: normalized.userID,
|
||||
userSig: normalized.userSig,
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
phase.value = 'dialing'
|
||||
statusText.value = '正在呼叫患者'
|
||||
|
||||
await TUICallKitAPI.calls({
|
||||
userIDList: [normalized.targetUserId],
|
||||
type: TUICallType.VIDEO_CALL,
|
||||
})
|
||||
void pollRoomId()
|
||||
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'status',
|
||||
diagnosisId: normalized.diagnosisId,
|
||||
status: 'dialing',
|
||||
})
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error)
|
||||
phase.value = 'error'
|
||||
statusText.value = message
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'error',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
message,
|
||||
})
|
||||
activeConfig = null
|
||||
endNotified = true
|
||||
throw new Error(message)
|
||||
} finally {
|
||||
starting = false
|
||||
}
|
||||
}
|
||||
|
||||
async function hangup(): Promise<void> {
|
||||
if (!activeConfig || endNotified) return
|
||||
|
||||
try {
|
||||
await TUICallKitAPI.hangup()
|
||||
notifyHangup('local-hangup')
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error)
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'error',
|
||||
diagnosisId: activeConfig.diagnosisId,
|
||||
message,
|
||||
})
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
window.doctorCall = { start, hangup }
|
||||
initializeQtWebChannel()
|
||||
|
||||
createApp(App, {
|
||||
phase: readonly(phase),
|
||||
statusText: readonly(statusText),
|
||||
}).mount('#app')
|
||||
|
||||
emit({ source: 'doctor-call', event: 'ready' })
|
||||
@@ -0,0 +1,127 @@
|
||||
:root {
|
||||
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
color: #f7f8fa;
|
||||
background: #0b0f14;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.call-stage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 35%, rgba(39, 74, 83, 0.22), transparent 38%),
|
||||
#0b0f14;
|
||||
}
|
||||
|
||||
.call-kit,
|
||||
.call-stage :is(.TUICallKit-desktop, .TUICallKit-mobile, #tuicallkit-id) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
position: absolute;
|
||||
inset: 50% auto auto 50%;
|
||||
display: grid;
|
||||
grid-template-columns: 12px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
width: min(520px, calc(100% - 48px));
|
||||
padding: 30px 32px;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
border-radius: 20px;
|
||||
background: rgba(19, 25, 32, 0.9);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.32);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: #8f9ba8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-card h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(22px, 3.2vw, 34px);
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.status-hint {
|
||||
margin: 14px 0 0;
|
||||
color: #9aa5b1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: 5px;
|
||||
border-radius: 50%;
|
||||
background: #77818c;
|
||||
box-shadow: 0 0 0 5px rgba(119, 129, 140, 0.12);
|
||||
}
|
||||
|
||||
.status-dot--starting,
|
||||
.status-dot--live {
|
||||
background: #52c99a;
|
||||
box-shadow: 0 0 0 5px rgba(82, 201, 154, 0.14);
|
||||
}
|
||||
|
||||
.status-dot--error {
|
||||
background: #f26d6d;
|
||||
box-shadow: 0 0 0 5px rgba(242, 109, 109, 0.14);
|
||||
}
|
||||
|
||||
.live-status {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 999px;
|
||||
background: rgba(11, 15, 20, 0.76);
|
||||
color: #e8edf2;
|
||||
font-size: 13px;
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.live-status .status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
target: 'chrome100',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user