更新软件升级

This commit is contained in:
扫地僧 2026-04-08 20:33:05 +08:00
parent 1bfa019634
commit a1f04f94ec
14 changed files with 1658 additions and 11 deletions

5
.gitignore vendored
View File

@ -9,8 +9,13 @@ lerna-debug.log*
node_modules node_modules
dist dist
output
dist-ssr dist-ssr
*.local *.local
output.zip
dist.zip
dist.7z
output.7z
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
"clean": "node scripts/clean-dist.mjs",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },

49
scripts/clean-dist.mjs Normal file
View File

@ -0,0 +1,49 @@
/**
* 构建前删除 dist带重试缓解 Windows Vite emptyDir EPERM文件被资源管理器预览杀毒vite preview 等占用
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const dirs = [path.join(root, "dist"), path.join(root, "output")];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function rmDirWithRetry(dir) {
for (let i = 0; i < 10; i++) {
if (!fs.existsSync(dir)) {
return true;
}
try {
fs.rmSync(dir, { recursive: true, force: true });
return true;
} catch (e) {
const code = e && typeof e === "object" && "code" in e ? e.code : "";
const retryable = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY";
if (!retryable || i === 9) {
console.error(`[clean-dist] 无法删除 ${path.relative(root, dir) || dir}:`, e instanceof Error ? e.message : e);
return false;
}
await sleep(350 * (i + 1));
}
}
return false;
}
async function main() {
let ok = true;
for (const dir of dirs) {
const r = await rmDirWithRetry(dir);
if (!r) ok = false;
}
if (!ok) {
console.error(
"请关闭占用上述目录的程序资源管理器预览、vite preview、IDE、杀毒实时扫描等后执行 npm run clean。"
);
process.exit(1);
}
}
await main();

78
src/api/complaint.js Normal file
View File

@ -0,0 +1,78 @@
import request from "@/utils/request";
/** 投诉建议列表 */
export function getComplaintList(params) {
return request({
url: "/platform/complaint/list",
method: "get",
params,
});
}
export function getComplaintDetail(id) {
return request({
url: `/platform/complaint/${id}`,
method: "get",
});
}
export function createComplaint(data) {
return request({
url: "/platform/complaint",
method: "post",
data,
});
}
export function updateComplaint(id, data) {
return request({
url: `/platform/complaint/${id}`,
method: "post",
data,
});
}
export function deleteComplaint(id) {
return request({
url: `/platform/complaint/${id}`,
method: "delete",
});
}
/** 产品分类(投诉建议用) */
export function getComplaintCategoryList() {
return request({
url: "/platform/complaintCategory/list",
method: "get",
});
}
export function getComplaintCategorySelect() {
return request({
url: "/platform/complaintCategory/select",
method: "get",
});
}
export function createComplaintCategory(data) {
return request({
url: "/platform/complaintCategory",
method: "post",
data,
});
}
export function updateComplaintCategory(id, data) {
return request({
url: `/platform/complaintCategory/${id}`,
method: "post",
data,
});
}
export function deleteComplaintCategory(id) {
return request({
url: `/platform/complaintCategory/${id}`,
method: "delete",
});
}

View File

@ -104,6 +104,7 @@ export function getFileById(id) {
* @param {Object} options 额外选项 * @param {Object} options 额外选项
* @param {string|number} [options.cate] 文件分组0 为未分类 * @param {string|number} [options.cate] 文件分组0 为未分类
* @param {string|number} [options.tuid] 租户用户 yz_tenant_user.id租户侧上传时传平台管理员不传 * @param {string|number} [options.tuid] 租户用户 yz_tenant_user.id租户侧上传时传平台管理员不传
* @param {(e: { loaded: number; total?: number }) => void} [options.onUploadProgress] 上传进度浏览器 XHR
* @returns {Promise} * @returns {Promise}
*/ */
export function uploadFile(formData, options = {}) { export function uploadFile(formData, options = {}) {
@ -115,14 +116,17 @@ export function uploadFile(formData, options = {}) {
formData.append("tuid", String(options.tuid)); formData.append("tuid", String(options.tuid));
} }
return request({ const config = {
url: "/platform/uploadfile", url: "/platform/uploadfile",
method: "post", method: "post",
data: formData, data: formData,
headers: { // 大安装包 / 极弱网2 小时;勿设置 Content-Type由浏览器自动带 boundary
"Content-Type": "multipart/form-data" timeout: 2 * 60 * 60 * 1000,
} };
}); if (typeof options.onUploadProgress === "function") {
config.onUploadProgress = options.onUploadProgress;
}
return request(config);
} }
/** /**

View File

@ -104,4 +104,21 @@ export function sendResetCode(data) {
method: "post", method: "post",
data, data,
}); });
}
// 租户端自助注册(/backend/*,与 go-platform routers/backend 一致)
export function register(data) {
return request({
url: "/backend/register",
method: "post",
data,
});
}
export function sendRegisterCode(data) {
return request({
url: "/backend/sendRegisterCode",
method: "post",
data,
});
} }

View File

@ -0,0 +1,39 @@
import request from "@/utils/request";
export function getSoftwareUpgradeList(params) {
return request({
url: "/platform/softwareupgrade/list",
method: "get",
params,
});
}
export function getSoftwareUpgradeDetail(id) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "get",
});
}
export function createSoftwareUpgrade(data) {
return request({
url: "/platform/softwareupgrade",
method: "post",
data,
});
}
export function updateSoftwareUpgrade(id, data) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "post",
data,
});
}
export function deleteSoftwareUpgrade(id) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "delete",
});
}

View File

@ -3,10 +3,10 @@ import axios from 'axios';
// 获取API基础URL开发环境可在 .env.development 留空,配合 Vite 代理访问 /platform // 获取API基础URL开发环境可在 .env.development 留空,配合 Vite 代理访问 /platform
const apiBaseURL = import.meta.env.VITE_API_BASE_URL ?? ""; const apiBaseURL = import.meta.env.VITE_API_BASE_URL ?? "";
// 创建axios实例 // 创建axios实例(普通接口 5min大文件上传在 api/file.js 单独更长 timeout
const service = axios.create({ const service = axios.create({
baseURL: apiBaseURL, baseURL: apiBaseURL,
timeout: 10000, timeout: 300000,
withCredentials: false // JWT 不需要 Cookie withCredentials: false // JWT 不需要 Cookie
}); });
@ -18,9 +18,12 @@ service.interceptors.request.use(
config.headers['Authorization'] = `Bearer ${token}`; config.headers['Authorization'] = `Bearer ${token}`;
} }
// 对于有 body 的请求POST、PUT、PATCH确保设置 Content-Type // 对于有 body 的请求POST、PUT、PATCH默认 JSONFormData 由浏览器带 multipart boundary不可手写 Content-Type
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) { if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
if (!config.headers['Content-Type'] && !config.headers['content-type']) { if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
delete config.headers['content-type'];
} else if (!config.headers['Content-Type'] && !config.headers['content-type']) {
config.headers['Content-Type'] = 'application/json'; config.headers['Content-Type'] = 'application/json';
} }
} }

View File

@ -0,0 +1,216 @@
<template>
<el-drawer
v-model="visible"
:title="isAdd ? '新增投诉建议' : '处理投诉建议'"
size="520px"
destroy-on-close
@closed="onClosed"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" v-loading="loading">
<el-form-item label="产品分类" prop="categoryId">
<el-select
v-model="form.categoryId"
placeholder="请选择"
style="width: 100%"
filterable
:disabled="!isAdd"
>
<el-option
v-for="c in categoryOptions"
:key="c.id"
:label="c.name"
:value="Number(c.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="标题" :disabled="!isAdd" />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="6"
placeholder="建议或投诉内容"
:disabled="!isAdd"
/>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contactName" :disabled="!isAdd" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.contactPhone" :disabled="!isAdd" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.contactEmail" :disabled="!isAdd" />
</el-form-item>
<template v-if="!isAdd">
<el-divider />
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" style="width: 100%">
<el-option :value="0" label="待处理" />
<el-option :value="1" label="处理中" />
<el-option :value="2" label="已回复" />
<el-option :value="3" label="已关闭" />
</el-select>
</el-form-item>
<el-form-item label="平台回复">
<el-input v-model="form.replyContent" type="textarea" :rows="5" placeholder="填写后保存将记录回复时间" />
</el-form-item>
<el-form-item label="内部备注">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, reactive, nextTick } from "vue";
import { ElMessage } from "element-plus";
import {
getComplaintDetail,
createComplaint,
updateComplaint,
getComplaintCategorySelect,
} from "@/api/complaint";
const emit = defineEmits(["saved"]);
const visible = ref(false);
const loading = ref(false);
const saving = ref(false);
const isAdd = ref(false);
const formRef = ref(null);
const categoryOptions = ref([]);
const form = reactive({
id: 0,
categoryId: null,
title: "",
content: "",
contactName: "",
contactPhone: "",
contactEmail: "",
status: 0,
replyContent: "",
remark: "",
});
const rules = {
categoryId: [{ required: true, message: "请选择产品分类", trigger: "change" }],
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
content: [{ required: true, message: "请输入内容", trigger: "blur" }],
};
async function loadCategories() {
const res = await getComplaintCategorySelect();
if (res?.code === 200 && Array.isArray(res.data)) {
categoryOptions.value = res.data;
} else {
categoryOptions.value = [];
}
}
function resetForm() {
form.id = 0;
form.categoryId = null;
form.title = "";
form.content = "";
form.contactName = "";
form.contactPhone = "";
form.contactEmail = "";
form.status = 0;
form.replyContent = "";
form.remark = "";
}
async function open(id) {
resetForm();
await loadCategories();
isAdd.value = !id;
visible.value = true;
await nextTick();
formRef.value?.clearValidate?.();
if (id) {
loading.value = true;
try {
const res = await getComplaintDetail(id);
if (res?.code !== 200 || !res.data) {
ElMessage.error(res?.msg || "加载失败");
visible.value = false;
return;
}
const d = res.data;
form.id = d.id;
form.categoryId = d.categoryId;
form.title = d.title || "";
form.content = d.content || "";
form.contactName = d.contactName || "";
form.contactPhone = d.contactPhone || "";
form.contactEmail = d.contactEmail || "";
form.status = d.status ?? 0;
form.replyContent = d.replyContent || "";
form.remark = d.remark || "";
} finally {
loading.value = false;
}
}
}
function onClosed() {
resetForm();
}
async function submit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch {
return;
}
saving.value = true;
try {
if (isAdd.value) {
const res = await createComplaint({
categoryId: form.categoryId,
title: form.title,
content: form.content,
contactName: form.contactName || undefined,
contactPhone: form.contactPhone || undefined,
contactEmail: form.contactEmail || undefined,
});
if (res?.code === 200) {
ElMessage.success("已创建");
visible.value = false;
emit("saved");
} else {
ElMessage.error(res?.msg || "失败");
}
} else {
const res = await updateComplaint(form.id, {
categoryId: form.categoryId,
status: form.status,
replyContent: form.replyContent,
remark: form.remark || undefined,
});
if (res?.code === 200) {
ElMessage.success("已保存");
visible.value = false;
emit("saved");
} else {
ElMessage.error(res?.msg || "失败");
}
}
} finally {
saving.value = false;
}
}
defineExpose({ open });
</script>

View File

@ -0,0 +1,492 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>投诉建议</h2>
<div class="header-actions">
<el-button @click="apiDocVisible = true">
<el-icon><Document /></el-icon>
接口说明
</el-button>
<el-button @click="openCategoryDialog">
<el-icon><Collection /></el-icon>
产品分类
</el-button>
<el-button type="primary" @click="editRef.open()">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button @click="fetchList" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider />
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="产品分类">
<el-select
v-model="searchForm.categoryId"
placeholder="全部"
clearable
style="width: 160px"
filterable
>
<el-option
v-for="c in categoryFilterOptions"
:key="c.id"
:label="c.name"
:value="Number(c.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 120px">
<el-option :value="0" label="待处理" />
<el-option :value="1" label="处理中" />
<el-option :value="2" label="已回复" />
<el-option :value="3" label="已关闭" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="标题/内容/联系方式"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" v-loading="loading" border style="width: 100%">
<el-table-column prop="id" label="ID" width="72" align="center" />
<el-table-column prop="categoryName" label="产品分类" width="120" align="center" />
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
<el-table-column label="联系人" width="100" align="center">
<template #default="{ row }">{{ row.contactName || "—" }}</template>
</el-table-column>
<el-table-column label="电话" width="120" align="center">
<template #default="{ row }">{{ row.contactPhone || "—" }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="提交时间" width="170" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="editRef.open(row.id)">处理</el-button>
<el-button text type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@current-change="fetchList"
@size-change="fetchList"
/>
</div>
<ComplaintEdit ref="editRef" @saved="fetchList" />
<!-- 接口说明列表页顶栏 -->
<el-drawer v-model="apiDocVisible" title="投诉建议 — 接口说明" direction="rtl" size="520px">
<el-scrollbar max-height="calc(100vh - 120px)">
<div class="api-doc">
<p class="api-doc-lead">
以下为 go-platform 已提供的 HTTP 接口请求前缀为当前环境配置的 API 地址开发环境示例见下方
</p>
<p><strong>API 根地址</strong></p>
<pre class="api-pre">{{ apiBase }}</pre>
<h4>鉴权</h4>
<p>除特别说明外均需平台管理员登录后携带</p>
<pre class="api-pre">Authorization: Bearer &lt;token&gt;</pre>
<p class="api-tip">Token <code>POST /platform/login</code> 返回列表分类管理处理投诉等均为平台端接口</p>
<h4>产品分类配置针对哪类产品提建议</h4>
<ul>
<li><code>GET /platform/complaintCategory/select</code> 仅返回 <code>status=1</code> 的分类供下拉/官网选分类用</li>
<li><code>GET /platform/complaintCategory/list</code> 全部分类管理列表</li>
<li><code>POST /platform/complaintCategory</code> 新增分类Body JSON<code>name</code>必填<code>code</code><code>sort</code><code>status</code></li>
<li><code>POST /platform/complaintCategory/:id</code> 更新</li>
<li><code>DELETE /platform/complaintCategory/:id</code> 软删</li>
</ul>
<h4>提交投诉建议</h4>
<p><code>POST /platform/complaint</code></p>
<p>BodyJSONcamelCase</p>
<pre class="api-pre">{
"categoryId": 1,
"title": "建议标题",
"content": "详细描述",
"contactName": "可选",
"contactPhone": "可选",
"contactEmail": "可选"
}</pre>
<p>成功示例<code>{ "code": 200, "msg": "success", "data": { "id": 1 } }</code></p>
<p class="api-tip">若需在官网/小程序<strong>免登录</strong>提交需在后端单独增加公开路由当前接口与平台管理一致 Bearer</p>
<h4>管理端列表与处理</h4>
<ul>
<li><code>GET /platform/complaint/list</code> 分页Query<code>page</code><code>pageSize</code><code>categoryId</code><code>status</code>0~3<code>keyword</code></li>
<li><code>GET /platform/complaint/:id</code> 详情</li>
<li><code>POST /platform/complaint/:id</code> 更新状态回复等<code>status</code><code>replyContent</code><code>remark</code><code>categoryId</code> </li>
<li><code>DELETE /platform/complaint/:id</code> 软删</li>
</ul>
<p><strong>状态</strong><code>0</code> 待处理 · <code>1</code> 处理中 · <code>2</code> 已回复 · <code>3</code> 已关闭</p>
<h4>数据表</h4>
<p><code>yz_system_complaint_category</code><code>yz_system_platform_complaint</code></p>
</div>
</el-scrollbar>
</el-drawer>
<!-- 产品分类管理 -->
<el-dialog v-model="categoryVisible" title="投诉建议 — 产品分类" width="640px" destroy-on-close>
<div class="cat-toolbar">
<el-button type="primary" size="small" @click="openCatForm()">新增分类</el-button>
<el-button size="small" @click="loadCategoryList">刷新</el-button>
</div>
<el-table :data="categoryRows" v-loading="catLoading" border size="small" style="margin-top: 12px">
<el-table-column prop="id" label="ID" width="64" align="center" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="code" label="编码" width="100" />
<el-table-column prop="sort" label="排序" width="72" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? "启用" : "禁用" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="openCatForm(row)">编辑</el-button>
<el-button text type="danger" size="small" @click="handleCatDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog
v-model="catFormVisible"
:title="catForm.id ? '编辑分类' : '新增分类'"
width="420px"
append-to-body
>
<el-form :model="catForm" label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="catForm.name" placeholder="如:官网、租户后台" />
</el-form-item>
<el-form-item label="编码">
<el-input v-model="catForm.code" placeholder="可选" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="catForm.sort" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="catForm.status">
<el-radio-button :value="1">启用</el-radio-button>
<el-radio-button :value="0">禁用</el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="catFormVisible = false">取消</el-button>
<el-button type="primary" :loading="catSaving" @click="saveCategory">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh, Collection, Document } from "@element-plus/icons-vue";
import {
getComplaintList,
deleteComplaint,
getComplaintCategoryList,
createComplaintCategory,
updateComplaintCategory,
deleteComplaintCategory,
} from "@/api/complaint";
import ComplaintEdit from "./components/edit.vue";
const editRef = ref(null);
const apiDocVisible = ref(false);
const apiBase = import.meta.env.VITE_API_BASE_URL || "";
const loading = ref(false);
const list = ref([]);
const categoryFilterOptions = ref([]);
const searchForm = reactive({
categoryId: null,
status: null,
keyword: "",
});
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
});
const categoryVisible = ref(false);
const catLoading = ref(false);
const categoryRows = ref([]);
const catFormVisible = ref(false);
const catSaving = ref(false);
const catForm = reactive({
id: 0,
name: "",
code: "",
sort: 0,
status: 1,
});
function statusText(s) {
const m = { 0: "待处理", 1: "处理中", 2: "已回复", 3: "已关闭" };
return m[s] ?? String(s);
}
function statusTag(s) {
if (s === 2) return "success";
if (s === 3) return "info";
if (s === 1) return "warning";
return "";
}
async function loadCategoryFilter() {
const res = await getComplaintCategoryList();
if (res?.code === 200 && Array.isArray(res.data)) {
categoryFilterOptions.value = res.data;
}
}
async function fetchList() {
loading.value = true;
try {
const params = {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
};
if (searchForm.categoryId != null && searchForm.categoryId !== "") {
params.categoryId = searchForm.categoryId;
}
if (searchForm.status !== null && searchForm.status !== "") {
params.status = searchForm.status;
}
const res = await getComplaintList(params);
if (res?.code === 200 && res.data) {
list.value = res.data.list || [];
pagination.total = res.data.total ?? 0;
} else {
list.value = [];
ElMessage.error(res?.msg || "加载失败");
}
} finally {
loading.value = false;
}
}
function handleSearch() {
pagination.page = 1;
fetchList();
}
function resetSearch() {
searchForm.categoryId = null;
searchForm.status = null;
searchForm.keyword = "";
pagination.page = 1;
fetchList();
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`确定删除「${row.title}」吗?`, "提示", { type: "warning" });
} catch {
return;
}
const res = await deleteComplaint(row.id);
if (res?.code === 200) {
ElMessage.success("已删除");
fetchList();
} else {
ElMessage.error(res?.msg || "删除失败");
}
}
function openCategoryDialog() {
categoryVisible.value = true;
loadCategoryList();
}
async function loadCategoryList() {
catLoading.value = true;
try {
const res = await getComplaintCategoryList();
if (res?.code === 200 && Array.isArray(res.data)) {
categoryRows.value = res.data;
}
} finally {
catLoading.value = false;
}
}
function openCatForm(row) {
if (row) {
catForm.id = row.id;
catForm.name = row.name || "";
catForm.code = row.code || "";
catForm.sort = row.sort ?? 0;
catForm.status = row.status ?? 1;
} else {
catForm.id = 0;
catForm.name = "";
catForm.code = "";
catForm.sort = 0;
catForm.status = 1;
}
catFormVisible.value = true;
}
async function saveCategory() {
if (!catForm.name.trim()) {
ElMessage.warning("请填写名称");
return;
}
catSaving.value = true;
try {
const body = {
name: catForm.name.trim(),
code: catForm.code?.trim() || undefined,
sort: catForm.sort,
status: catForm.status,
};
let res;
if (catForm.id) {
res = await updateComplaintCategory(catForm.id, body);
} else {
res = await createComplaintCategory(body);
}
if (res?.code === 200) {
ElMessage.success("已保存");
catFormVisible.value = false;
await loadCategoryList();
await loadCategoryFilter();
} else {
ElMessage.error(res?.msg || "失败");
}
} finally {
catSaving.value = false;
}
}
async function handleCatDelete(row) {
try {
await ElMessageBox.confirm(`删除分类「${row.name}」?已有投诉引用时请谨慎。`, "提示", { type: "warning" });
} catch {
return;
}
const res = await deleteComplaintCategory(row.id);
if (res?.code === 200) {
ElMessage.success("已删除");
loadCategoryList();
loadCategoryFilter();
} else {
ElMessage.error(res?.msg || "删除失败");
}
}
onMounted(() => {
loadCategoryFilter();
fetchList();
});
</script>
<style scoped>
.container-box {
padding: 16px;
}
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-actions {
display: flex;
gap: 8px;
}
.search-form {
margin-bottom: 12px;
}
.pager {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.cat-toolbar {
display: flex;
gap: 8px;
}
.api-doc {
padding-right: 12px;
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-regular);
}
.api-doc h4 {
margin: 18px 0 8px;
font-size: 14px;
color: var(--el-text-color-primary);
}
.api-doc ul {
margin: 8px 0;
padding-left: 1.2em;
}
.api-doc code {
font-size: 12px;
padding: 0 4px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.api-pre {
margin: 8px 0;
padding: 10px 12px;
font-size: 12px;
line-height: 1.5;
background: var(--el-fill-color-light);
border-radius: 6px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.api-doc-lead {
margin: 0 0 12px;
}
.api-tip {
margin-top: 8px;
padding: 8px 10px;
font-size: 12px;
background: var(--el-color-info-light-9);
border-radius: 6px;
color: var(--el-text-color-secondary);
}
</style>

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,463 @@
<template>
<el-drawer
v-model="visible"
:title="isAdd ? '新增软件产品' : '编辑软件产品'"
size="560px"
destroy-on-close
@closed="onClosed"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" v-loading="loading">
<el-form-item label="软件名称" prop="name">
<el-input v-model="form.name" placeholder="如:桌面客户端" />
</el-form-item>
<el-form-item label="产品 code" prop="code">
<el-input
v-model="form.code"
placeholder="客户端检查更新时传的 code唯一"
:disabled="!isAdd"
/>
</el-form-item>
<el-form-item label="最新版本号" prop="latestVersion">
<el-input v-model="form.latestVersion" placeholder="如 1.2.0" />
</el-form-item>
<el-form-item label="安装包">
<el-upload
ref="uploadRef"
class="pkg-upload"
:limit="1"
:file-list="displayFileList"
:http-request="handlePackageUpload"
:on-remove="onRemovePackage"
@exceed="onExceedReplace"
accept=".zip,.exe,.dmg,.msi,.msix,.apk,.deb,.rpm,.7z,.pkg,.tar,.gz"
>
<el-button type="primary" :loading="uploading">
{{ form.fileId ? "重新上传安装包" : "上传安装包" }}
</el-button>
</el-upload>
<div v-if="uploadXHRActive" class="upload-progress-wrap">
<el-progress
:percentage="uploadPercent"
:indeterminate="uploadIndeterminate"
:stroke-width="10"
striped
striped-flow
/>
<div class="upload-progress-text">
<template v-if="!uploadIndeterminate">
<span>{{ uploadPercent }}%</span>
<span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template>
<template v-else>
<span>上传中</span>
<template v-if="uploadSpeedText && uploadSpeedText !== '—'">
<span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span>
</template>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template>
</div>
</div>
<div v-if="form.fileId" class="pkg-meta">
<span>文件 ID<code>{{ form.fileId }}</code></span>
</div>
</el-form-item>
<el-form-item label="下载/更新地址">
<el-input
v-model="form.downloadUrl"
type="textarea"
:rows="2"
placeholder="上传后自动填写;可改为 CDN 等完整 URL"
/>
</el-form-item>
<el-form-item label="强制更新">
<el-switch v-model="form.forceUpdate" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="更新说明">
<el-input v-model="form.releaseNotes" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio-button :value="1">启用</el-radio-button>
<el-radio-button :value="0">停用</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item v-if="!isAdd && resolvedUrl" label="当前解析地址">
<el-link :href="resolvedUrl" type="primary" target="_blank">{{ resolvedUrl }}</el-link>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, reactive, nextTick } from "vue";
import { ElMessage } from "element-plus";
import {
getSoftwareUpgradeDetail,
createSoftwareUpgrade,
updateSoftwareUpgrade,
} from "@/api/softwareUpgrade";
import { getUserCate, createFileCate, uploadFile, getFileById } from "@/api/file";
const emit = defineEmits(["saved"]);
const visible = ref(false);
const loading = ref(false);
const saving = ref(false);
const uploading = ref(false);
const uploadXHRActive = ref(false);
const uploadPercent = ref(0);
const uploadIndeterminate = ref(false);
const uploadSpeedText = ref("—");
const uploadSizeText = ref("");
const isAdd = ref(false);
const resolvedUrl = ref("");
const formRef = ref(null);
const uploadRef = ref(null);
const displayFileList = ref([]);
const uploadedLabel = ref("");
const appsUpgradeCateId = ref(null);
const form = reactive({
id: 0,
name: "",
code: "",
latestVersion: "0.0.1",
fileId: null,
downloadUrl: "",
forceUpdate: 0,
releaseNotes: "",
status: 1,
sort: 0,
});
const rules = {
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
code: [{ required: true, message: "请输入产品标识", trigger: "blur" }],
latestVersion: [{ required: true, message: "请输入版本号", trigger: "blur" }],
};
function formatBytes(n) {
if (n == null || !Number.isFinite(n) || n < 0) return "";
if (n < 1024) return `${Math.round(n)} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10240 ? 2 : 1)} KB`;
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`;
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
/** @param {number} bps bytes per second */
function formatSpeed(bps) {
if (bps == null || !Number.isFinite(bps) || bps <= 0) return "—";
return `${formatBytes(bps)}/s`;
}
function absoluteFromSrc(src) {
if (!src) return "";
const s = String(src).trim();
if (/^https?:\/\//i.test(s)) return s;
const base = (import.meta.env.VITE_API_BASE_URL || "").replace(/\/$/, "");
const path = s.startsWith("/") ? s : `/${s}`;
return base ? `${base}${path}` : path;
}
async function ensureAppsUpgradeCate() {
if (appsUpgradeCateId.value != null) {
return appsUpgradeCateId.value;
}
const res = await getUserCate();
if (res?.code !== 200 || !Array.isArray(res.data)) {
return null;
}
const found = res.data.find(
(c) => String(c.name || "").trim().toLowerCase() === "appsupgrade"
);
if (found && found.id) {
appsUpgradeCateId.value = Number(found.id);
return appsUpgradeCateId.value;
}
const cr = await createFileCate({ name: "appsupgrade" });
if (cr?.code === 200 && cr.data?.id != null) {
appsUpgradeCateId.value = Number(cr.data.id);
return appsUpgradeCateId.value;
}
ElMessage.error(cr?.msg || "创建文件分组 appsupgrade 失败");
return null;
}
function reset() {
form.id = 0;
form.name = "";
form.code = "";
form.latestVersion = "0.0.1";
form.fileId = null;
form.downloadUrl = "";
form.forceUpdate = 0;
form.releaseNotes = "";
form.status = 1;
form.sort = 0;
resolvedUrl.value = "";
displayFileList.value = [];
uploadedLabel.value = "";
uploadXHRActive.value = false;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
uploadRef.value?.clearFiles?.();
}
function onClosed() {
reset();
}
function onRemovePackage() {
form.fileId = null;
form.downloadUrl = "";
uploadedLabel.value = "";
displayFileList.value = [];
}
/** Element Plus超出 limit 时传入待加入的文件列表(一般为 File[] */
function onExceedReplace(files) {
const first = files[0];
const raw = first?.raw != null ? first.raw : first;
uploadRef.value?.clearFiles?.();
form.fileId = null;
form.downloadUrl = "";
uploadedLabel.value = "";
displayFileList.value = [];
if (raw) {
handlePackageUpload({ file: raw, onSuccess: () => {}, onError: () => {} });
}
}
async function handlePackageUpload(options) {
const file = options.file?.raw != null ? options.file.raw : options.file;
if (!file) {
options.onError?.(new Error("无文件"));
return;
}
uploading.value = true;
try {
const cateId = await ensureAppsUpgradeCate();
if (!cateId) {
options.onError?.(new Error("no category"));
return;
}
const fd = new FormData();
fd.append("file", file);
uploadXHRActive.value = true;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
const xhrStart = Date.now();
let lastLoaded = 0;
let lastTick = xhrStart;
let emaBps = 0;
const res = await uploadFile(fd, {
cate: cateId,
onUploadProgress: (e) => {
const now = Date.now();
const loaded = e.loaded;
const total = e.total ?? 0;
const elapsedSec = (now - xhrStart) / 1000;
const dt = (now - lastTick) / 1000;
if (dt >= 0.07 && loaded >= lastLoaded) {
const inst = (loaded - lastLoaded) / dt;
emaBps = emaBps > 0 ? emaBps * 0.72 + inst * 0.28 : inst;
lastLoaded = loaded;
lastTick = now;
}
const avgBps = elapsedSec > 0.12 ? loaded / elapsedSec : 0;
const showBps = emaBps > 0 ? emaBps : avgBps;
uploadSpeedText.value = showBps > 0 ? formatSpeed(showBps) : elapsedSec > 0.05 ? formatSpeed(avgBps) : "—";
if (total > 0) {
uploadIndeterminate.value = false;
uploadPercent.value = Math.min(100, Math.round((loaded * 100) / total));
uploadSizeText.value = `${formatBytes(loaded)} / ${formatBytes(total)}`;
} else {
uploadIndeterminate.value = true;
uploadSizeText.value = loaded > 0 ? `已上传 ${formatBytes(loaded)}` : "";
}
},
});
if (res?.code === 200 || res?.code === 201) {
const d = res.data || {};
form.fileId = d.id != null ? Number(d.id) : null;
const src = d.url || "";
form.downloadUrl = absoluteFromSrc(src);
uploadedLabel.value = d.name || file.name || "安装包";
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
ElMessage.success(res.code === 201 ? "文件已存在,已关联" : "上传成功");
options.onSuccess?.(res);
} else {
ElMessage.error(res?.msg || "上传失败");
options.onError?.(new Error(res?.msg));
}
} catch (e) {
ElMessage.error(e?.message || "上传失败");
options.onError?.(e);
} finally {
uploading.value = false;
uploadXHRActive.value = false;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
}
}
async function open(id) {
reset();
isAdd.value = !id;
visible.value = true;
appsUpgradeCateId.value = null;
await nextTick();
formRef.value?.clearValidate?.();
if (!id) return;
loading.value = true;
try {
const res = await getSoftwareUpgradeDetail(id);
if (res?.code !== 200 || !res.data) {
ElMessage.error(res?.msg || "加载失败");
visible.value = false;
return;
}
const d = res.data;
form.id = d.id;
form.name = d.name || "";
form.code = d.code || "";
form.latestVersion = d.latestVersion || "0.0.1";
form.fileId = d.fileId != null ? Number(d.fileId) : null;
form.downloadUrl = d.downloadUrl || d.resolvedDownloadUrl || "";
form.forceUpdate = d.forceUpdate ? 1 : 0;
form.releaseNotes = d.releaseNotes || "";
form.status = d.status !== 0 ? 1 : 0;
form.sort = d.sort ?? 0;
resolvedUrl.value = d.resolvedDownloadUrl || "";
if (form.fileId) {
try {
const fr = await getFileById(form.fileId);
if (fr?.code === 200 && fr.data) {
uploadedLabel.value = fr.data.name || `文件 #${form.fileId}`;
} else {
uploadedLabel.value = `文件 #${form.fileId}`;
}
} catch {
uploadedLabel.value = `文件 #${form.fileId}`;
}
displayFileList.value = [
{ name: uploadedLabel.value, uid: `exist-${form.fileId}` },
];
}
} finally {
loading.value = false;
}
}
async function submit() {
try {
await formRef.value?.validate();
} catch {
return;
}
const payload = {
name: form.name.trim(),
code: form.code.trim(),
latestVersion: form.latestVersion.trim(),
downloadUrl: form.downloadUrl.trim() || undefined,
forceUpdate: form.forceUpdate,
releaseNotes: form.releaseNotes.trim() || undefined,
status: form.status,
sort: form.sort,
};
if (form.fileId != null && form.fileId !== "" && Number(form.fileId) > 0) {
payload.fileId = Number(form.fileId);
} else {
payload.fileId = 0;
}
saving.value = true;
try {
let res;
if (isAdd.value) {
res = await createSoftwareUpgrade(payload);
} else {
res = await updateSoftwareUpgrade(form.id, payload);
}
if (res?.code === 200) {
ElMessage.success("已保存");
visible.value = false;
emit("saved");
} else {
ElMessage.error(res?.msg || "失败");
}
} finally {
saving.value = false;
}
}
defineExpose({ open });
</script>
<style scoped>
.hint {
margin: 0 0 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.55;
}
.hint code {
padding: 0 4px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.pkg-upload {
width: 100%;
}
.upload-progress-wrap {
margin-top: 10px;
width: 100%;
}
.upload-progress-text {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
word-break: break-all;
}
.upload-progress-text .upload-sep {
margin: 0 0.35em;
opacity: 0.55;
}
.pkg-meta {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.pkg-meta code {
font-size: 12px;
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>软件升级</h2>
<div class="header-actions">
<el-button @click="apiDocVisible = true">
<el-icon><Document /></el-icon>
接口说明
</el-button>
<el-button type="primary" @click="editRef.open()">
<el-icon><Plus /></el-icon>
新增产品
</el-button>
<el-button @click="fetchList" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider />
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="名称 / 产品标识"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" v-loading="loading" border style="width: 100%">
<el-table-column prop="id" label="ID" width="72" align="center" />
<el-table-column prop="name" label="软件名称" min-width="140" />
<el-table-column prop="code" label="产品code" width="140" />
<el-table-column prop="latestVersion" label="最新版本" width="100" align="center" />
<el-table-column label="文件 ID" width="88" align="center">
<template #default="{ row }">{{ row.fileId ?? "—" }}</template>
</el-table-column>
<el-table-column label="解析下载地址" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link v-if="row.resolvedDownloadUrl" :href="row.resolvedDownloadUrl" type="primary" target="_blank">
{{ row.resolvedDownloadUrl }}
</el-link>
<span v-else class="muted"></span>
</template>
</el-table-column>
<el-table-column prop="forceUpdate" label="强制更新" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.forceUpdate ? 'danger' : 'info'">{{ row.forceUpdate ? "是" : "否" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? "启用" : "停用" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="72" align="center" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="editRef.open(row.id)">编辑</el-button>
<el-button text type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@current-change="fetchList"
@size-change="fetchList"
/>
</div>
<SoftwareEdit ref="editRef" @saved="fetchList" />
<el-drawer v-model="apiDocVisible" title="软件升级 — 接口说明" direction="rtl" size="540px">
<el-scrollbar max-height="calc(100vh - 120px)">
<div class="api-doc">
<p class="lead">客户端启动时调用开放接口比对版本管理端接口需平台管理员 Token</p>
<p><strong>API 根地址</strong></p>
<pre class="api-pre">{{ apiBase }}</pre>
<h4>客户端检查更新无需登录</h4>
<p><code>GET /api/softwareupgrade/check</code></p>
<p>Query</p>
<ul>
<li><code>code</code>必填与后台配置的产品标识一致</li>
<li><code>version</code>可选当前客户端版本缺省按 <code>0.0.0</code></li>
</ul>
<p>响应 <code>data</code> 字段说明</p>
<ul>
<li><code>upToDate</code>是否已是最新当前版本 后台最新版本</li>
<li><code>latestVersion</code>后台配置的最新版本号</li>
<li><code>downloadUrl</code>安装包地址自定义 URL 或由文件 ID 解析</li>
<li><code>forceUpdate</code>是否建议强制更新</li>
<li><code>releaseNotes</code>更新说明</li>
</ul>
<pre class="api-pre">GET {{ apiBase }}/api/softwareupgrade/check?code=my-app&amp;version=1.0.0</pre>
<h4>版本号规则</h4>
<p> <code>.</code> 分段数字比较 <code>1.10.0</code> &gt; <code>1.9.0</code>后缀 <code>-beta</code> 等仅截取 <code>-</code> 前参与比较</p>
<h4>安装包上传</h4>
<p>软件升级编辑抽屉内点击 <strong>上传安装包</strong>会调用 <code>POST /platform/uploadfile</code>并自动归入文件分组 <code>appsupgrade</code>不存在则自动创建保存后会把 <strong>file_id</strong> 与根据 <code>VITE_API_BASE_URL</code> 拼好的 <strong>download_url</strong> 写入数据库</p>
<p>静态资源映射<code>/uploads</code>生产环境请配置正确的 API 公网地址与反向代理头<code>X-Forwarded-Proto</code> </p>
<h4>管理端接口 Bearer</h4>
<ul>
<li><code>GET /platform/softwareupgrade/list</code> <code>page</code><code>pageSize</code><code>keyword</code></li>
<li><code>GET /platform/softwareupgrade/:id</code></li>
<li><code>POST /platform/softwareupgrade</code> 新增</li>
<li><code>POST /platform/softwareupgrade/:id</code> 更新</li>
<li><code>DELETE /platform/softwareupgrade/:id</code> 软删</li>
</ul>
<p><code>yz_system_software_upgrade</code></p>
</div>
</el-scrollbar>
</el-drawer>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh, Document } from "@element-plus/icons-vue";
import { getSoftwareUpgradeList, deleteSoftwareUpgrade } from "@/api/softwareUpgrade";
import SoftwareEdit from "./components/edit.vue";
const editRef = ref(null);
const apiDocVisible = ref(false);
const apiBase = import.meta.env.VITE_API_BASE_URL || "";
const loading = ref(false);
const list = ref([]);
const searchForm = reactive({ keyword: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
async function fetchList() {
loading.value = true;
try {
const res = await getSoftwareUpgradeList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
});
if (res?.code === 200 && res.data) {
list.value = res.data.list || [];
pagination.total = res.data.total ?? 0;
} else {
list.value = [];
ElMessage.error(res?.msg || "加载失败");
}
} finally {
loading.value = false;
}
}
function handleSearch() {
pagination.page = 1;
fetchList();
}
function resetSearch() {
searchForm.keyword = "";
pagination.page = 1;
fetchList();
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`确定删除「${row.name}」吗?`, "提示", { type: "warning" });
} catch {
return;
}
const res = await deleteSoftwareUpgrade(row.id);
if (res?.code === 200) {
ElMessage.success("已删除");
fetchList();
} else {
ElMessage.error(res?.msg || "删除失败");
}
}
onMounted(() => {
fetchList();
});
</script>
<style scoped>
.container-box {
padding: 16px;
}
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-actions {
display: flex;
gap: 8px;
}
.search-form {
margin-bottom: 12px;
}
.pager {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.muted {
color: var(--el-text-color-placeholder);
}
.api-doc {
padding-right: 12px;
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-regular);
}
.api-doc h4 {
margin: 18px 0 8px;
font-size: 14px;
color: var(--el-text-color-primary);
}
.api-doc ul {
margin: 8px 0;
padding-left: 1.2em;
}
.api-doc code {
font-size: 12px;
padding: 0 4px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.api-pre {
margin: 8px 0;
padding: 10px 12px;
font-size: 12px;
line-height: 1.5;
background: var(--el-fill-color-light);
border-radius: 6px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.lead {
margin: 0 0 12px;
}
</style>

View File

@ -7,6 +7,11 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
// Windows 下若 dist 被占用,清空或覆盖会 EPERM产物默认写到 output/(与 dist 分离)。部署时请同步指向 output。
build: {
outDir: "output",
emptyOutDir: true,
},
plugins: [ plugins: [
vue(), vue(),
AutoImport({ AutoImport({
@ -23,10 +28,14 @@ export default defineConfig({
}, },
server: { server: {
port: 5000, port: 5000,
// 开发时前端在 5000接口走相对路径 /platform/*,由这里转发到 Gogo/conf/app.conf 默认 httpport=8080 // 开发时前端在 5000接口走相对路径 /platform/*、/backend/*,转发到本地 Go当前 httpport=8081
proxy: { proxy: {
"/platform": { "/platform": {
target: "http://127.0.0.1:8080", target: "http://127.0.0.1:8081",
changeOrigin: true,
},
"/backend": {
target: "http://127.0.0.1:8081",
changeOrigin: true, changeOrigin: true,
}, },
}, },