@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>YMhut Unified Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1328
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ymhut-unified-setup",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"lucide-vue-next": "^0.468.0",
|
||||
"vite": "^6.3.5",
|
||||
"vue": "^3.5.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Database,
|
||||
FolderCog,
|
||||
LoaderCircle,
|
||||
LockKeyhole,
|
||||
ServerCog,
|
||||
ShieldCheck,
|
||||
TriangleAlert,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
type SetupStatus = {
|
||||
ok: boolean;
|
||||
initialized: boolean;
|
||||
baseDir: string;
|
||||
configPath: string;
|
||||
defaults?: {
|
||||
provider?: string;
|
||||
sqlitePath?: string;
|
||||
mysqlDsn?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TestResult = {
|
||||
ok: boolean;
|
||||
provider: string;
|
||||
normalized?: Record<string, unknown>;
|
||||
maskedDsn?: string;
|
||||
latencyMs?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ title: "环境确认", description: "确认服务基准目录和公开地址", icon: ServerCog },
|
||||
{ title: "数据库选择", description: "选择 SQLite 或 MySQL", icon: Database },
|
||||
{ title: "连接配置", description: "填写对应数据库参数", icon: FolderCog },
|
||||
{ title: "连接测试", description: "由后端真实测试连接", icon: ShieldCheck },
|
||||
{ title: "完成确认", description: "写入配置并创建默认账号", icon: LockKeyhole },
|
||||
];
|
||||
|
||||
const currentStep = ref(0);
|
||||
const loading = ref(false);
|
||||
const status = ref<SetupStatus | null>(null);
|
||||
const testResult = ref<TestResult | null>(null);
|
||||
const error = ref("");
|
||||
const completed = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
baseUrl: "https://update.ymhut.cn",
|
||||
provider: "sqlite",
|
||||
sqliteDir: "storage",
|
||||
sqliteFile: "unified.sqlite",
|
||||
mysql: {
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
database: "ymhut_unified",
|
||||
username: "",
|
||||
password: "",
|
||||
charset: "utf8mb4",
|
||||
parseTime: true,
|
||||
tls: "false",
|
||||
},
|
||||
});
|
||||
|
||||
const sqlitePath = computed(() => {
|
||||
const dir = form.sqliteDir.trim().replace(/[\\/]+$/g, "") || "storage";
|
||||
const file = form.sqliteFile.trim() || "unified.sqlite";
|
||||
return `${dir}/${file}`.replace(/\\/g, "/");
|
||||
});
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (currentStep.value === 0) return form.baseUrl.trim().length > 0;
|
||||
if (currentStep.value === 1) return form.provider === "sqlite" || form.provider === "mysql";
|
||||
if (currentStep.value === 2) {
|
||||
if (form.provider === "sqlite") return form.sqliteDir.trim() && form.sqliteFile.trim();
|
||||
return form.mysql.host.trim() && form.mysql.port > 0 && form.mysql.database.trim() && form.mysql.username.trim();
|
||||
}
|
||||
if (currentStep.value === 3) return testResult.value?.ok === true;
|
||||
return true;
|
||||
});
|
||||
|
||||
const payload = computed(() => ({
|
||||
provider: form.provider,
|
||||
baseUrl: form.baseUrl.trim(),
|
||||
sqlitePath: sqlitePath.value,
|
||||
mysql: {
|
||||
host: form.mysql.host.trim(),
|
||||
port: Number(form.mysql.port || 3306),
|
||||
database: form.mysql.database.trim(),
|
||||
username: form.mysql.username.trim(),
|
||||
password: form.mysql.password,
|
||||
charset: form.mysql.charset.trim() || "utf8mb4",
|
||||
parseTime: form.mysql.parseTime,
|
||||
tls: form.mysql.tls.trim() || "false",
|
||||
},
|
||||
}));
|
||||
|
||||
async function api<T>(target: string, init: RequestInit = {}): Promise<T> {
|
||||
const headers = new Headers(init.headers);
|
||||
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
||||
const res = await fetch(target, { ...init, headers });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || data.ok === false) throw new Error(data.message || data.error || `HTTP ${res.status}`);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
status.value = await api<SetupStatus>("/api/setup/status");
|
||||
form.baseUrl = status.value.defaults?.baseUrl || form.baseUrl;
|
||||
form.provider = status.value.defaults?.provider || "sqlite";
|
||||
const defaultSQLite = status.value.defaults?.sqlitePath || "storage/unified.sqlite";
|
||||
const normalized = defaultSQLite.replace(/\\/g, "/");
|
||||
const index = normalized.lastIndexOf("/");
|
||||
if (index > -1) {
|
||||
form.sqliteDir = normalized.slice(0, index) || "storage";
|
||||
form.sqliteFile = normalized.slice(index + 1) || "unified.sqlite";
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (!canContinue.value) return;
|
||||
if (currentStep.value < steps.length - 1) currentStep.value += 1;
|
||||
}
|
||||
|
||||
function previousStep() {
|
||||
if (currentStep.value > 0) currentStep.value -= 1;
|
||||
}
|
||||
|
||||
async function testDatabase() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
testResult.value = null;
|
||||
try {
|
||||
testResult.value = await api<TestResult>("/api/setup/database/test", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload.value),
|
||||
});
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function completeSetup() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
await api("/api/setup/complete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload.value),
|
||||
});
|
||||
completed.value = true;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadStatus);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="setup-shell">
|
||||
<section class="setup-card">
|
||||
<aside class="setup-aside">
|
||||
<p class="eyebrow">update.ymhut.cn</p>
|
||||
<h1>初始化统一管理服务端</h1>
|
||||
<p>使用分步向导确认运行目录、数据库连接和默认账号。所有连接测试都由服务端完成。</p>
|
||||
|
||||
<ol class="step-list">
|
||||
<li v-for="(step, index) in steps" :key="step.title" :class="{ active: currentStep === index, done: currentStep > index || completed }">
|
||||
<span><component :is="step.icon" :size="18" /></span>
|
||||
<div>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<small>{{ step.description }}</small>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
|
||||
<section class="setup-main">
|
||||
<header class="section-head">
|
||||
<div>
|
||||
<p class="eyebrow">Step {{ currentStep + 1 }} / {{ steps.length }}</p>
|
||||
<h2>{{ steps[currentStep].title }}</h2>
|
||||
</div>
|
||||
<span v-if="loading" class="badge"><LoaderCircle :size="15" class="spin" />处理中</span>
|
||||
</header>
|
||||
|
||||
<p v-if="error" class="alert bad"><TriangleAlert :size="17" />{{ error }}</p>
|
||||
|
||||
<section v-if="completed" class="complete-panel">
|
||||
<CheckCircle2 :size="42" />
|
||||
<h2>初始化完成</h2>
|
||||
<p>配置已写入服务基准目录。请重启服务后打开 <code>/admin/login</code>,使用默认账号 <strong>admin/admin</strong> 登录并立即修改密码。</p>
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="currentStep === 0" class="form-grid">
|
||||
<label class="wide">服务基准目录<input :value="status?.baseDir || '-'" readonly /></label>
|
||||
<label class="wide">配置文件路径<input :value="status?.configPath || '-'" readonly /></label>
|
||||
<label class="wide">规范服务地址<input v-model.trim="form.baseUrl" placeholder="https://update.ymhut.cn" /></label>
|
||||
<p class="hint wide">相对路径会以服务基准目录为准。生产环境可继续通过反向代理把其他域名重定向到该地址。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 1" class="choice-grid">
|
||||
<button :class="{ selected: form.provider === 'sqlite' }" @click="form.provider = 'sqlite'; testResult = null">
|
||||
<Database :size="24" />
|
||||
<strong>SQLite</strong>
|
||||
<span>推荐单机或轻量部署。默认写入 storage/unified.sqlite。</span>
|
||||
</button>
|
||||
<button :class="{ selected: form.provider === 'mysql' }" @click="form.provider = 'mysql'; testResult = null">
|
||||
<ServerCog :size="24" />
|
||||
<strong>MySQL</strong>
|
||||
<span>适合远端热同步、迁移和多实例备份场景。</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 2 && form.provider === 'sqlite'" class="form-grid">
|
||||
<label>SQLite 目录<input v-model.trim="form.sqliteDir" placeholder="storage" /></label>
|
||||
<label>SQLite 文件名<input v-model.trim="form.sqliteFile" placeholder="unified.sqlite" /></label>
|
||||
<label class="wide">最终路径<input :value="sqlitePath" readonly /></label>
|
||||
<p class="hint wide">保存时仍会由后端校验并创建目录,不允许路径逃逸。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 2 && form.provider === 'mysql'" class="form-grid">
|
||||
<label>Host<input v-model.trim="form.mysql.host" placeholder="127.0.0.1" /></label>
|
||||
<label>Port<input v-model.number="form.mysql.port" type="number" min="1" /></label>
|
||||
<label>Database<input v-model.trim="form.mysql.database" placeholder="ymhut_unified" /></label>
|
||||
<label>Username<input v-model.trim="form.mysql.username" autocomplete="username" /></label>
|
||||
<label>Password<input v-model="form.mysql.password" type="password" autocomplete="new-password" /></label>
|
||||
<label>Charset<input v-model.trim="form.mysql.charset" placeholder="utf8mb4" /></label>
|
||||
<label>parseTime<select v-model="form.mysql.parseTime"><option :value="true">true</option><option :value="false">false</option></select></label>
|
||||
<label>TLS<select v-model="form.mysql.tls"><option>false</option><option>true</option><option>skip-verify</option><option>preferred</option></select></label>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 3" class="test-panel">
|
||||
<div class="summary-box">
|
||||
<span>数据库类型</span><strong>{{ form.provider }}</strong>
|
||||
<span>SQLite 路径</span><strong>{{ form.provider === "sqlite" ? sqlitePath : "-" }}</strong>
|
||||
<span>MySQL 地址</span><strong>{{ form.provider === "mysql" ? `${form.mysql.host}:${form.mysql.port}/${form.mysql.database}` : "-" }}</strong>
|
||||
</div>
|
||||
<button class="btn primary" data-testid="setup-test-database" @click="testDatabase"><ShieldCheck :size="17" />执行连接测试</button>
|
||||
<div v-if="testResult" class="result good">
|
||||
<CheckCircle2 :size="20" />
|
||||
<div>
|
||||
<strong>连接测试通过</strong>
|
||||
<p>{{ testResult.provider }},耗时 {{ testResult.latencyMs || 0 }}ms</p>
|
||||
<code v-if="testResult.maskedDsn">{{ testResult.maskedDsn }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep === 4" class="confirm-panel">
|
||||
<div class="summary-box">
|
||||
<span>服务地址</span><strong>{{ form.baseUrl }}</strong>
|
||||
<span>数据库</span><strong>{{ form.provider }}</strong>
|
||||
<span>连接</span><strong>{{ form.provider === "sqlite" ? sqlitePath : `${form.mysql.host}:${form.mysql.port}/${form.mysql.database}` }}</strong>
|
||||
<span>默认账号</span><strong>admin / admin</strong>
|
||||
</div>
|
||||
<p class="alert warn"><TriangleAlert :size="17" />初始化不会在此处修改默认密码。进入后台后登录页会提示默认密码,请立即修改。</p>
|
||||
<button class="btn primary" data-testid="setup-complete" @click="completeSetup"><CheckCircle2 :size="17" />写入配置并完成初始化</button>
|
||||
</div>
|
||||
|
||||
<footer class="setup-actions">
|
||||
<button class="btn ghost" data-testid="setup-prev" :disabled="currentStep === 0" @click="previousStep"><ChevronLeft :size="17" />上一步</button>
|
||||
<button v-if="currentStep < steps.length - 1" class="btn primary" data-testid="setup-next" :disabled="!canContinue" @click="nextStep">下一步<ChevronRight :size="17" /></button>
|
||||
</footer>
|
||||
</template>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./styles.css";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -0,0 +1,190 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: "Microsoft YaHei UI", "Segoe UI", Arial, sans-serif;
|
||||
color: #e5eefb;
|
||||
background: #0b1220;
|
||||
--panel: rgba(15, 23, 42, 0.9);
|
||||
--panel-soft: rgba(30, 41, 59, 0.72);
|
||||
--line: rgba(148, 163, 184, 0.28);
|
||||
--muted: #94a3b8;
|
||||
--ink: #f8fafc;
|
||||
--primary: #22c55e;
|
||||
--blue: #38bdf8;
|
||||
--warn: #fbbf24;
|
||||
--bad: #fb7185;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; background: #0b1220; }
|
||||
button, input, select { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
|
||||
.setup-shell {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at 10% 10%, rgba(56, 189, 248, 0.15), transparent 32%),
|
||||
radial-gradient(circle at 82% 18%, rgba(34, 197, 94, 0.16), transparent 34%),
|
||||
linear-gradient(145deg, #0b1220, #111827 52%, #0f172a);
|
||||
}
|
||||
|
||||
.setup-card {
|
||||
width: min(1120px, 100%);
|
||||
min-height: 680px;
|
||||
display: grid;
|
||||
grid-template-columns: 360px minmax(0, 1fr);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(2, 6, 23, 0.74);
|
||||
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.36);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setup-aside {
|
||||
padding: 28px;
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.68));
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.setup-aside h1 { margin: 0 0 16px; font-size: 38px; line-height: 1.08; letter-spacing: 0; }
|
||||
.setup-aside p { color: var(--muted); line-height: 1.75; }
|
||||
.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.step-list { list-style: none; margin: 28px 0 0; padding: 0; display: grid; gap: 12px; }
|
||||
.step-list li {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.54);
|
||||
}
|
||||
.step-list li > span {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #1e293b;
|
||||
color: var(--muted);
|
||||
}
|
||||
.step-list li.active { border-color: rgba(34, 197, 94, 0.75); background: rgba(34, 197, 94, 0.08); }
|
||||
.step-list li.done > span, .step-list li.active > span { color: #052e16; background: var(--primary); }
|
||||
.step-list strong { display: block; }
|
||||
.step-list small { color: var(--muted); }
|
||||
|
||||
.setup-main { padding: 28px; display: flex; flex-direction: column; gap: 18px; }
|
||||
.section-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
||||
.section-head h2 { margin: 0; font-size: 26px; }
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 5px 9px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
.spin { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.wide { grid-column: 1 / -1; }
|
||||
label { display: grid; gap: 7px; color: #cbd5e1; font-weight: 800; font-size: 13px; }
|
||||
input, select {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
background: #020617;
|
||||
color: var(--ink);
|
||||
padding: 9px 11px;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15); }
|
||||
input[readonly] { color: var(--muted); }
|
||||
.hint { margin: 0; color: var(--muted); line-height: 1.65; }
|
||||
|
||||
.choice-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.choice-grid button {
|
||||
min-height: 190px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
color: var(--ink);
|
||||
padding: 18px;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
}
|
||||
.choice-grid button.selected { border-color: var(--primary); background: rgba(34, 197, 94, 0.09); }
|
||||
.choice-grid span { color: var(--muted); line-height: 1.6; }
|
||||
|
||||
.summary-box {
|
||||
display: grid;
|
||||
grid-template-columns: 140px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-soft);
|
||||
padding: 16px;
|
||||
}
|
||||
.summary-box span { color: var(--muted); }
|
||||
.summary-box strong { overflow-wrap: anywhere; }
|
||||
.test-panel, .confirm-panel, .complete-panel { display: grid; gap: 16px; }
|
||||
.complete-panel {
|
||||
min-height: 420px;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.complete-panel svg { color: var(--primary); }
|
||||
.complete-panel p { max-width: 620px; color: var(--muted); line-height: 1.8; }
|
||||
code { color: #bfdbfe; }
|
||||
|
||||
.btn {
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-weight: 900;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
}
|
||||
.btn.primary { background: var(--primary); color: #052e16; border-color: var(--primary); }
|
||||
.btn.ghost { color: #cbd5e1; background: #1e293b; }
|
||||
.setup-actions { margin-top: auto; display: flex; justify-content: space-between; gap: 10px; }
|
||||
.alert, .result {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.alert.bad { color: #fecdd3; background: rgba(244, 63, 94, 0.12); border: 1px solid rgba(244, 63, 94, 0.36); }
|
||||
.alert.warn { color: #fde68a; background: rgba(245, 158, 11, 0.12); border: 1px solid rgba(245, 158, 11, 0.36); }
|
||||
.result.good { color: #bbf7d0; background: rgba(34, 197, 94, 0.12); border: 1px solid rgba(34, 197, 94, 0.36); }
|
||||
.result p { margin: 4px 0; color: var(--muted); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.setup-card { grid-template-columns: 1fr; }
|
||||
.setup-aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.form-grid, .choice-grid { grid-template-columns: 1fr; }
|
||||
.summary-box { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/setup/",
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:33550"
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user