Compare commits
4
Commits
ec53c34b9c
...
0a93441f64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a93441f64 | ||
|
|
b1dba84c49 | ||
|
|
efd55a3a75 | ||
|
|
5296b5999c |
@@ -1,9 +1,17 @@
|
||||
# 开发环境 (dev):仅 vite dev 加载(--mode dev)。值可自由指本地或线上地址。
|
||||
# build 产物不读此文件(build 默认 production mode,只认 .env / .env.production,两者均不存在),
|
||||
# 线上环境差异全部由 projman 的 env-config-{目标}.json 承担。
|
||||
#
|
||||
# 本地起服务的三种模式(详见 vite.config.ts 的 server.proxy 注释):
|
||||
# 纯前端 mock: VITE_IS_MOCK=true npm run dev → 用户/菜单走 src/assets/mock,其余接口透传给后端
|
||||
# 本地后端联调: npm run dev → 跨源直连本文件 VITE_SERVICE_HOST
|
||||
# 线上后端联调: VITE_SERVICE_HOST= YH_API_PROXY_TARGET=http://base.yunhaixingtong.com/userbase npm run dev
|
||||
# (留空 VITE_SERVICE_HOST 让请求走同源 /api,由 dev 代理转发;CAS 登录 cookie 跨域带不过去时改用本地后端)
|
||||
VITE_BASE_HOST=http://base.bestsch.com
|
||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||
VITE_SERVICE_HOST=http://localhost:8060
|
||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
||||
VITE_IS_MOCK=false
|
||||
# 主题通道 dev 跨源直连(可选,需通道已开 CORS);留空则同源相对 → 走 vite 的 /static 代理
|
||||
VITE_THEME_HOST=
|
||||
|
||||
+20
@@ -27,6 +27,26 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- 主题通道引导(A 类免插件接入,见 YschTheme《管理端主题远程化与公共静态通道方案》§11):
|
||||
唯一接入点。不装 npm 依赖、不改构建配置;FOUC 门、manifest 预取、主题 <link> 追加与
|
||||
排序(晚于 viewuiplus.css)全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,
|
||||
本应用无需重新构建。
|
||||
origin 零写死,优先级四档:__APP_CONFIG__.staticHost(专用通道覆盖)
|
||||
> __APP_CONFIG__.baseHost(env-config 在线维护,换域名改配置即生效)
|
||||
> %VITE_THEME_HOST%(仅 dev 注入的跨源直连,需通道已开 CORS)
|
||||
> 同源相对 "/static/…"(生产常态:本应用部署在 base 域 /userbase/front/,与通道同源;
|
||||
本地 dev 由 vite.config 的 /static 代理兜到通道)。
|
||||
document.write 是有意为之:保证 parser-blocking,让档位类与 FOUC 门先于首帧;
|
||||
build 不读 .env,占位符留字面量 → 判首字符 % 置空 → 产物零环境值烧入。 -->
|
||||
<script>
|
||||
(function () {
|
||||
var c = window.__APP_CONFIG__ || {};
|
||||
var themeHost = "%VITE_THEME_HOST%";
|
||||
if (themeHost.charAt(0) === "%") themeHost = "";
|
||||
var host = String(c.staticHost || c.baseHost || themeHost).replace(/\/+$/, "");
|
||||
document.write('<script src="' + host + '/static/js/yh-theme-init.js"><\/script>');
|
||||
})();
|
||||
</script>
|
||||
<title><%- title %></title>
|
||||
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
||||
<script src="./libs/jquery.min.js"></script>
|
||||
|
||||
Generated
+4
-4
@@ -16,7 +16,7 @@
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.1.4",
|
||||
"sass": "^1.64.1",
|
||||
"view-ui-plus": "^1.3.16",
|
||||
"view-ui-plus": "^1.3.24",
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vue": "^3.4.19",
|
||||
"vue-router": "4",
|
||||
@@ -8354,9 +8354,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/view-ui-plus": {
|
||||
"version": "1.3.21",
|
||||
"resolved": "https://registry.npmmirror.com/view-ui-plus/-/view-ui-plus-1.3.21.tgz",
|
||||
"integrity": "sha512-WvMd6klBnhgVjxJpNn/8Bpa/+aEWv1Ii5cMKvXMnsTebbOyaM6Qm4hEUBglXA/MPlTfcop7YkGC4eHHM+x3iCg==",
|
||||
"version": "1.3.24",
|
||||
"resolved": "https://registry.npmmirror.com/view-ui-plus/-/view-ui-plus-1.3.24.tgz",
|
||||
"integrity": "sha512-umhDHnCPGIHCQYYyZDw3AfGSQqlUThR4/eEZjtzCFrrjLzXpE6I8eIggzZQGKHK3zKzRTopDYJsyeFb+K9MLuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async-validator": "^3.3.0",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.1.4",
|
||||
"sass": "^1.64.1",
|
||||
"view-ui-plus": "^1.3.16",
|
||||
"view-ui-plus": "^1.3.24",
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vue": "^3.4.19",
|
||||
"vue-router": "4",
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ const setCurrentRole = (user: any) => {
|
||||
};
|
||||
localStorage.setItem("currentRole", encodeURI(JSON.stringify(currentRole)));
|
||||
|
||||
// 主题样式已本地化到 src/assets/styles/layout.css(含 default-theme / base.layout.beta / _iview_override),不再请求远程样式
|
||||
// 主题样式由中央静态通道下发(index.html 的 yh-theme-init.js 挂 /static/*),本仓不再持有 layout.css
|
||||
|
||||
if (user?.type === 1) {
|
||||
store.setUserRole(UserRoleEnum.SuperAdmin);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"prefix": "appicon",
|
||||
"info": {
|
||||
"name": "AppIcon",
|
||||
"total": 2,
|
||||
"author": "ysch front-end team",
|
||||
"license": {
|
||||
"title": "Remix Icon (Apache-2.0) 等,见 meta.json source",
|
||||
"spdx": "Apache-2.0"
|
||||
},
|
||||
"height": 1024
|
||||
},
|
||||
"icons": {
|
||||
"tongzhi": {
|
||||
"body": "<path d=\"M853.33 725.33H938.67V810.67H85.33V725.33H170.67V426.67C170.67 238.15 323.49 85.33 512 85.33C700.51 85.33 853.33 238.15 853.33 426.67V725.33ZM768 725.33V426.67C768 285.28 653.38 170.67 512 170.67C370.62 170.67 256 285.28 256 426.67V725.33H768ZM384 896H640V981.33H384V896Z\" ></path>",
|
||||
"cn": "通知"
|
||||
},
|
||||
"wenjuan": {
|
||||
"body": "<path d=\"M725.33 85.33V170.67H853.61C877.02 170.67 896 189.65 896 213.05V896.28C896 919.69 877.02 938.67 853.61 938.67H170.39C146.98 938.67 128 919.68 128 896.28V213.05C128 189.64 146.98 170.67 170.39 170.67H298.67V85.33H725.33ZM298.67 256H213.33V853.33H810.67V256H725.33V341.33H298.67V256ZM384 682.67V768H298.67V682.67H384ZM384 554.67V640H298.67V554.67H384ZM384 426.67V512H298.67V426.67H384ZM640 170.67H384V256H640V170.67Z\" ></path>",
|
||||
"cn": "问卷"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div class="appicon-picker">
|
||||
<!-- 当前选中态 / 清除 -->
|
||||
<div class="picker-head">
|
||||
<div class="picked" v-if="modelValue">
|
||||
<span class="picked-glyph scene-tile" v-html="svgOf(modelValue)"></span>
|
||||
<span class="picked-info">
|
||||
<span class="picked-cn">{{ cnOf(modelValue) }}</span>
|
||||
<span class="picked-key">{{ modelValue }}</span>
|
||||
</span>
|
||||
<Icon type="ios-close" class="picked-clear" title="清除(回到位图/默认回退链)" @click.stop="clearPick" />
|
||||
</div>
|
||||
<Button v-else size="small" icon="md-add" @click="open = true">选择矢量图标</Button>
|
||||
<Button v-if="modelValue" size="small" class="ml-8" @click="open = true">更换</Button>
|
||||
</div>
|
||||
|
||||
<!-- 着色:不指定 = 端侧默认(PC 白色 / App 分组色) -->
|
||||
<div class="color-row" v-if="modelValue">
|
||||
<span class="color-label">着色:</span>
|
||||
<Tooltip content="空 = 跟随端侧场景默认(PC 白图标、App 分组色),一般无需指定" placement="top" transfer>
|
||||
<span class="help-icon">?</span>
|
||||
</Tooltip>
|
||||
<div class="swatches">
|
||||
<div class="swatch swatch-none" :class="{ active: !color }" title="端侧默认" @click="pickColor('')">无</div>
|
||||
<div v-for="c in PRESET_COLORS" :key="c" class="swatch" :class="{ active: color === c }"
|
||||
:style="{ background: c }" :title="c" @click="pickColor(c)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-model="open" title="选择矢量图标(AppIcon 资产库)" width="640" footer-hide transfer :mask-closable="true">
|
||||
<div class="picker-body">
|
||||
<Input v-model="keyword" prefix="ios-search" placeholder="搜索中文名或图标名" clearable />
|
||||
<div class="picker-grid">
|
||||
<div v-for="item in filtered" :key="item.key" class="picker-cell"
|
||||
:class="{ active: item.key === modelValue }" @click="pick(item.key)">
|
||||
<span class="scene-tile" v-html="svgOf(item.key)"></span>
|
||||
<div class="cell-cn">{{ item.cn }}</div>
|
||||
<div class="cell-key">{{ item.key }}</div>
|
||||
</div>
|
||||
<div class="picker-empty" v-if="!filtered.length">无匹配图标(清单未拉取成功时仅剩内置副本)</div>
|
||||
</div>
|
||||
<div class="picker-tip">
|
||||
三场景同形:方块底(PC 应用卡)/ 圆底(App 首页)/ 线性(App 列表)——底色由各端场景决定,此处仅选形状。
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import builtinManifest from "@/assets/appicon-icons.json";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
|
||||
/**
|
||||
* 矢量图标选择器(AppIcon 资产库)。
|
||||
* 清单来源:可配置 manifestUrl(默认线上 /static/appicon/icons.json),拉取失败回落本地内置副本
|
||||
* (P1 静态应用未部署时管理端依然可用;副本与资产库 icons.json 同步,见资产库 build_dist)。
|
||||
* 预览直接内联 svg body(不依赖字体加载),tile/circle/plain 三场景同形。
|
||||
*/
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 矢量图标键(AppIcon 图标名),空 = 不使用矢量图标 */
|
||||
modelValue?: string;
|
||||
/** 着色 CSS 色值,空 = 端侧默认 */
|
||||
color?: string;
|
||||
/** 清单地址(默认平台静态通道) */
|
||||
manifestUrl?: string;
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
color: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:color']);
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
interface IconEntry { key: string; cn: string; body: string }
|
||||
|
||||
const open = ref(false);
|
||||
const keyword = ref('');
|
||||
const remoteIcons = ref<IconEntry[] | null>(null);
|
||||
|
||||
const PRESET_COLORS = ['#366ef4', '#3fa45b', '#f2994a', '#7c5cc4', '#17b8a6', '#ff5757'];
|
||||
|
||||
const builtinIcons: IconEntry[] = Object.entries((builtinManifest as any).icons || {}).map(
|
||||
([key, v]: [string, any]) => ({ key, cn: v?.cn || key, body: v?.body || '' })
|
||||
);
|
||||
|
||||
const icons = computed<IconEntry[]>(() => remoteIcons.value ?? builtinIcons);
|
||||
|
||||
const filtered = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
if (!kw) return icons.value;
|
||||
return icons.value.filter(x => x.cn.includes(kw) || x.key.includes(kw));
|
||||
});
|
||||
|
||||
function cnOf(key: string): string {
|
||||
return icons.value.find(x => x.key === key)?.cn || key;
|
||||
}
|
||||
|
||||
function bodyOf(key: string): string {
|
||||
return icons.value.find(x => x.key === key)?.body || '';
|
||||
}
|
||||
|
||||
function svgOf(key: string): string {
|
||||
const body = bodyOf(key);
|
||||
if (!body) return '';
|
||||
return `<svg viewBox="0 0 1024 1024" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;display:block">${body}</svg>`;
|
||||
}
|
||||
|
||||
function pick(key: string) {
|
||||
emit('update:modelValue', key);
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function clearPick() {
|
||||
emit('update:modelValue', '');
|
||||
emit('update:color', '');
|
||||
}
|
||||
|
||||
function pickColor(c: string) {
|
||||
emit('update:color', c);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 线上清单优先(P1 部署后自动生效);失败静默回落内置副本
|
||||
try {
|
||||
const url = props.manifestUrl || (($conf?.baseHost || '') + '/static/appicon/icons.json');
|
||||
const res = await fetch(url, { credentials: 'omit' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const list = Object.entries(data?.icons || {}).map(
|
||||
([key, v]: [string, any]) => ({ key, cn: v?.cn || key, body: v?.body || '' })
|
||||
);
|
||||
if (list.length) remoteIcons.value = list;
|
||||
} catch {
|
||||
// 回落内置副本,不提示(本地开发 baseHost 指向本地时必然 404)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.appicon-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.picker-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.picked {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 10px 4px 4px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 6px;
|
||||
|
||||
.picked-cn {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.picked-key {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
font-family: Consolas, monospace;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.picked-clear {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picked-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.color-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.color-label {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid #c5c8ce;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
color: #808695;
|
||||
cursor: help;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.swatches {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.active {
|
||||
border-color: #577afb;
|
||||
box-shadow: 0 0 0 1px #577afb;
|
||||
}
|
||||
|
||||
&.swatch-none {
|
||||
background: #fff;
|
||||
border: 1px dashed #c5c8ce;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #808695;
|
||||
|
||||
&.active {
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-body {
|
||||
.picker-grid {
|
||||
margin-top: 12px;
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.picker-cell {
|
||||
text-align: center;
|
||||
padding: 10px 6px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #577afb;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #577afb;
|
||||
box-shadow: 0 0 0 1px #577afb;
|
||||
}
|
||||
|
||||
.cell-cn {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.cell-key {
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-empty {
|
||||
grid-column: 1 / -1;
|
||||
color: #bbb;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.picker-tip {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 选中态/网格预览的方块场景(与端侧 tile token 同构,仅管理端演示用) */
|
||||
.scene-tile {
|
||||
--appicon-bg: #366ef4;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 18%;
|
||||
background: var(--appicon-bg);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts" setup>
|
||||
/* 默认头像三态组件(YschTheme 公共源,scripts/sync-apps.mjs 下发三端 src/components/UserAvatar.vue):
|
||||
头像 URL 非空 → <img>;否则 style==="text" 且能取到字 → 首字;否则 → 剪影(纯 CSS/SVG,零资源文件)。
|
||||
取值链/取字契约见《设置页修复与默认头像落地方案》§10.1/§10.2;样式全在本文件 <style scoped>(唯一 CSS 源,
|
||||
不进通道 icons.src.css——通道 CSS 对 UserHome 不可达,内联分发才能消双份漂移,见同方案 §10.4)。
|
||||
根元素即 .yh-avatar 圆:尺寸内联必须挂圆本身(挂外层 span 会被 flex 拉成椭圆,§12 踩坑)。 */
|
||||
import { computed } from "vue";
|
||||
import { pickAvatarText, resolveAvatarStyle } from "@/utils/avatar";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 头像 URL:非空直接 <img>(三态第一优先) */
|
||||
src?: string | null;
|
||||
/** 取字来源(首字态用):经 pickAvatarText 判定,取不到字回落剪影 */
|
||||
name?: string | null;
|
||||
/** 直径 px;font-size = round(size * 0.44) */
|
||||
size?: number;
|
||||
/** 配色场景:light=页面浅底(品牌实底+白内容);brand=header 品牌色底(剪影 S1 玻璃底 / 首字 S2 白底反色,§10.3 分档) */
|
||||
placement?: "light" | "brand";
|
||||
}>(),
|
||||
{ src: null, name: null, size: 28, placement: "light" },
|
||||
);
|
||||
|
||||
const text = computed(() => (props.src ? "" : pickAvatarText(props.name)));
|
||||
const style = computed(() => resolveAvatarStyle());
|
||||
/** 首字态 = style==="text" 且能取到字(契约 §10.2:二者缺一即剪影,取到字但档位是剪影时绝不能出字) */
|
||||
const showText = computed(() => style.value === "text" && !!text.value);
|
||||
|
||||
const rootClass = computed(() => [
|
||||
"yh-avatar",
|
||||
props.placement === "brand" ? "yh-avatar--brand" : "yh-avatar--light",
|
||||
{ "is-text": showText.value },
|
||||
]);
|
||||
|
||||
const rootStyle = computed(() => ({
|
||||
width: `${props.size}px`,
|
||||
height: `${props.size}px`,
|
||||
fontSize: `${Math.round(props.size * 0.44)}px`,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="rootClass" :style="rootStyle">
|
||||
<img v-if="src" class="yh-avatar__img" :src="src" alt="" />
|
||||
<template v-else-if="showText">{{ text }}</template>
|
||||
<svg v-else class="yh-avatar__sil" viewBox="0 0 1024 1024" aria-hidden="true">
|
||||
<circle cx="512" cy="399" r="174" />
|
||||
<ellipse cx="512" cy="946" rx="420" ry="240" />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.yh-avatar {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
.yh-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
/* 剪影 SVG:容器圆 overflow:hidden 裁切肩部溢出(禁用 clipPath);overflow:visible 放行 viewBox 外形状 */
|
||||
.yh-avatar__sil {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
fill: currentColor;
|
||||
}
|
||||
/* 页面内(浅底)——品牌实底 + 白内容 */
|
||||
.yh-avatar--light {
|
||||
background: var(--brand, #577afb);
|
||||
color: #fff;
|
||||
}
|
||||
/* header(品牌色底)——与页面内反向:剪影档 = S1 半透明白,首字档 = S2 纯白底 + 品牌色字 */
|
||||
.yh-avatar--brand {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
}
|
||||
.yh-avatar--brand.is-text {
|
||||
background: #fff;
|
||||
color: var(--brand, #577afb);
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
let props = defineProps<{ layoutData: LayoutData }>();
|
||||
const reload: Function = inject("reload") || new Function("console.error('empty')");
|
||||
|
||||
@@ -138,7 +139,8 @@ const linkMenu = (page: string) => {
|
||||
};
|
||||
|
||||
const linkUserInfo = () => {
|
||||
window.location.href = baseHost + "/v/userinfo";
|
||||
// 头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(默认头像轮 2026-09-30,原 /v/userinfo 已下线语义)
|
||||
window.location.href = baseHost + "/userhome/front/index.html#/settings?pane=profile";
|
||||
};
|
||||
const logout = () => {
|
||||
let url = window.location.href;
|
||||
@@ -174,7 +176,7 @@ function linkHomePage() {
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="school-bg"></div>
|
||||
<div class="header-aside fr">
|
||||
<img :src="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
|
||||
<UserAvatar class="fl align-middle" :src="userInfo.avatar" :name="userInfo.name" :size="28" placement="brand" @click="linkUserInfo" />
|
||||
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
||||
{{ userInfo.name }}
|
||||
</a>
|
||||
|
||||
+3
-2
@@ -13,8 +13,9 @@ import mittBus from "@/utils/mitt";
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
|
||||
import "@/assets/styles/main.scss";
|
||||
// 本地化的布局/主题样式(原远程 base.layout.beta / _iview_override / default-theme 合并)
|
||||
import "@/assets/styles/layout.css";
|
||||
// 布局/主题/图标(base.layout.css + iview.override.css + icons.css)由中央静态通道下发:
|
||||
// index.html 的 yh-theme-init.js 在 DOMContentLoaded 后按 manifest 版本追加 <link>,
|
||||
// 天然排在本行组件库样式与 main.scss 之后(原本地 layout.css 是通道旧版全量拷贝,已删)
|
||||
//导入自定义指令
|
||||
import directive from "@/utils/directive/index";
|
||||
|
||||
|
||||
@@ -14,7 +14,14 @@ function closeLoading() {
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = conf.serviceHost;
|
||||
/**
|
||||
* mock 模式必须短路成同源相对路径,否则 alova 会带着 serviceHost(本地 8060 / 线上域)跨源直连,
|
||||
* vite-plugin-mock 的中间件根本拦不到——这正是 `VITE_IS_MOCK=true` 开了却仍报 "Failed to fetch" 的根因
|
||||
* (axios 那条老链路的 api.config.ts 认 isMock,迁到 alova 后这一处漏了)。
|
||||
* 同源之后:mock 注册过的接口(/api/base/GetUserInfo、GetUserMod)由 mock 应答,
|
||||
* 未注册的接口继续经 vite.config 的 /api 代理透传给后端(见 YH_API_PROXY_TARGET)。
|
||||
*/
|
||||
const baseUrl = conf.isMock ? "" : conf.serviceHost;
|
||||
|
||||
/* 创建带 View Design UI 的 Alova 实例 */
|
||||
export const alovaInstance = createBschAlova(baseUrl, {
|
||||
|
||||
@@ -19,7 +19,8 @@ function closeLoading() {
|
||||
}
|
||||
|
||||
// const baseUrl = conf.serviceHost + "/api";
|
||||
const baseUrl = conf.serviceHost + "/mobiapi";
|
||||
// 同 alova-iview:mock 模式短路成同源,否则 vite-plugin-mock / dev 代理都拦不到跨源请求
|
||||
const baseUrl = (conf.isMock ? "" : conf.serviceHost) + "/mobiapi";
|
||||
/* 创建带 Vant UI 的 Alova 实例 */
|
||||
export const alovaInstance = createBschAlova(baseUrl, {
|
||||
injectHeaders(method) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 默认头像样式(首字 / 剪影)公共源(YschTheme,scripts/sync-apps.mjs 下发三端 src/utils/avatar.ts)。
|
||||
* 取值链与主题同构(theme-core.js resolveEffectiveThemeState 的合成链写法):
|
||||
* URL ?yh-avatar=text|silhouette(仅预览/排障覆盖:只影响读、绝不写回)
|
||||
* > localStorage["yh-avatar"](用户自选,纯字符串)
|
||||
* > localStorage["yh-avatar-sch-default"](学校默认,UserHome 登录后写入)
|
||||
* > "text"(系统默认)
|
||||
* 值域恒为 "text" | "silhouette",键缺失/值非法均静默回落下一级,不报错不阻塞渲染。
|
||||
* 渲染三态与取字规则的唯一契约见 UserHome《设置页修复与默认头像落地方案》§10.1/§10.2。
|
||||
*/
|
||||
|
||||
export type AvatarStyle = "text" | "silhouette";
|
||||
|
||||
interface StorageLike {
|
||||
getItem(k: string): string | null;
|
||||
setItem(k: string, v: string): void;
|
||||
removeItem(k: string): void;
|
||||
}
|
||||
|
||||
export const AVATAR_STORAGE_KEY = "yh-avatar";
|
||||
|
||||
/** 学校默认头像存储键(UserHome 登录时写入;值格式与用户键相同,仅无"用户自选"语义) */
|
||||
export const SCHOOL_DEFAULT_AVATAR_STORAGE_KEY = "yh-avatar-sch-default";
|
||||
|
||||
export const AVATAR_STYLES: readonly AvatarStyle[] = ["text", "silhouette"];
|
||||
|
||||
export const DEFAULT_AVATAR_STYLE: AvatarStyle = "text";
|
||||
|
||||
/** 值合法才采用(用户键/学校键同一判据;脏数据不生效) */
|
||||
function readAvatarStyleOrNull(raw: string | null): AvatarStyle | null {
|
||||
return raw === "text" || raw === "silhouette" ? raw : null;
|
||||
}
|
||||
|
||||
/** URL 覆盖参数(?yh-avatar=text|silhouette):非法值忽略,且只影响"读哪一档"、绝不写回 localStorage */
|
||||
function readAvatarOverride(): AvatarStyle | null {
|
||||
try {
|
||||
const search = (globalThis.location && globalThis.location.search) || "";
|
||||
const m = new RegExp("[?&]yh-avatar=([^&]*)").exec(search);
|
||||
return m ? readAvatarStyleOrNull(decodeURIComponent(m[1])) : null;
|
||||
} catch {
|
||||
/* 无 location 环境(测试/SSR):无覆盖 */
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生效样式合成(与 resolveEffectiveThemeState 同构):
|
||||
* URL 覆盖 > localStorage["yh-avatar"](用户自选)> localStorage["yh-avatar-sch-default"](学校默认)
|
||||
* > DEFAULT_AVATAR_STYLE。
|
||||
*/
|
||||
export function resolveAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): AvatarStyle {
|
||||
let style: AvatarStyle | null = null;
|
||||
if (storage) {
|
||||
style = readAvatarStyleOrNull(storage.getItem(AVATAR_STORAGE_KEY));
|
||||
if (!style) style = readAvatarStyleOrNull(storage.getItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY));
|
||||
}
|
||||
if (!style) style = DEFAULT_AVATAR_STYLE;
|
||||
return readAvatarOverride() || style;
|
||||
}
|
||||
|
||||
/** 保存用户自选(直写 localStorage,无响应式;调用方操作后自行刷新 UI) */
|
||||
export function saveAvatarStyle(style: AvatarStyle, storage: StorageLike | undefined = globalThis.localStorage): void {
|
||||
try {
|
||||
storage?.setItem(AVATAR_STORAGE_KEY, style);
|
||||
} catch {
|
||||
/* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 跟随学校默认 = 删用户自选键(之后回落学校默认/系统默认) */
|
||||
export function clearAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): void {
|
||||
try {
|
||||
storage?.removeItem(AVATAR_STORAGE_KEY);
|
||||
} catch {
|
||||
/* 隐私模式静默 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取字(顺序判定,先命中先返回):含 "@" 先截 "@" 前段(邮箱取首字母);
|
||||
* 首字符必须用展开 [...s][0](禁用 charAt(0),避免代理对截半出乱码)。
|
||||
* ASCII 字母转大写;纯数字回落 ""(剪影);符号/emoji/空白回落 ""(剪影);其余(CJK 等)原样。
|
||||
*/
|
||||
export function pickAvatarText(name: string | null | undefined): string {
|
||||
let s = String(name ?? "");
|
||||
const at = s.indexOf("@");
|
||||
if (at > -1) s = s.slice(0, at);
|
||||
const ch = [...s.trim()][0];
|
||||
if (ch === undefined) return "";
|
||||
if (/^[A-Za-z]$/.test(ch)) return ch.toUpperCase();
|
||||
if (/^[0-9]$/.test(ch)) return "";
|
||||
if (/^[\p{P}\p{S}\s]$/u.test(ch)) return "";
|
||||
return ch;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ let onSuccessCallback: (() => void) | null = null;
|
||||
// useForm 托管提交
|
||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
||||
(dto) => Apis.AppSettings.addApp({ data: dto }),
|
||||
{ initialForm: { name: '', categoryId: undefined, desc: '', appCode: '', appNature: 'BUSINESS', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||
{ initialForm: { name: '', categoryId: undefined, desc: '', appCode: '', appNature: 'BUSINESS', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', iconKey: '', iconColor: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||
);
|
||||
|
||||
onSuccess(() => {
|
||||
@@ -57,6 +57,8 @@ async function handleSubmit(formData: Record<string, any>) {
|
||||
pcIcon: formData.pcIcon,
|
||||
appUrl: formData.appUrl,
|
||||
appIcon: formData.appIcon,
|
||||
iconKey: formData.iconKey,
|
||||
iconColor: formData.iconColor,
|
||||
officialAccountUrl: formData.officialAccountUrl,
|
||||
dingtalkUrl: formData.dingtalkUrl,
|
||||
};
|
||||
|
||||
+101
-22
@@ -86,14 +86,14 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第五行:Pc应用地址 / Pc应用图标(基石应用整行隐藏——不进门户网格,图标无消费方;学校模式仅保留图标可改) -->
|
||||
<!-- 第五行:Pc应用地址 / 应用图标(平台模式=AppIcon 矢量选择器;学校模式保留位图上传——学校覆盖线只收 pcIcon) -->
|
||||
<Row v-if="!natureBase">
|
||||
<Col span="12" v-if="!schoolMode">
|
||||
<FormItem label="Pc应用地址:">
|
||||
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<Col span="12" v-if="schoolMode">
|
||||
<FormItem label="Pc应用图标:">
|
||||
<Upload :on-success="(res: any) => onUploadSuccess(res, 'pcIcon')" :show-upload-list="false"
|
||||
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
|
||||
@@ -109,30 +109,68 @@
|
||||
</Upload>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span="12" v-else>
|
||||
<FormItem label="应用图标:">
|
||||
<AppIconPicker v-model="form.iconKey" v-model:color="form.iconColor" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第六行:App接口地址 / App应用图标(学校模式/基石应用隐藏) -->
|
||||
<!-- 第六行:App接口地址(学校模式/基石应用隐藏;App位图图标移入下方高级折叠区) -->
|
||||
<Row v-if="!schoolMode && !natureBase">
|
||||
<Col span="12">
|
||||
<FormItem label="App接口地址:">
|
||||
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<FormItem label="App应用图标:">
|
||||
<Upload :on-success="(res: any) => onUploadSuccess(res, 'appIcon')" :show-upload-list="false"
|
||||
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
|
||||
:with-credentials="true" :headers="{ yschapirequest: 1 }">
|
||||
<div v-if="!form.appIcon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon @click.stop="form.appIcon = null" class="cloase-btn" name="widget-close" size="18"
|
||||
color="#fff" />
|
||||
<img :src="previewUrl(form.appIcon)" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
</FormItem>
|
||||
</Row>
|
||||
|
||||
<!-- 位图上传(高级 · 遗留通道):存量 pc_icon/app_icon 位图链路保留,新应用优先矢量图标键 -->
|
||||
<Row v-if="!schoolMode && !natureBase">
|
||||
<Col span="24">
|
||||
<Collapse>
|
||||
<Panel name="bitmap">
|
||||
上传位图图标(高级 · 遗留通道)
|
||||
<template #content>
|
||||
<div class="bitmap-upload-row">
|
||||
<div class="bitmap-upload-item">
|
||||
<div class="bitmap-label">Pc应用图标:</div>
|
||||
<Upload :on-success="(res: any) => onUploadSuccess(res, 'pcIcon')"
|
||||
:before-upload="beforeBitmapUpload" :show-upload-list="false"
|
||||
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
|
||||
:with-credentials="true" :headers="{ yschapirequest: 1 }">
|
||||
<div v-if="!form.pcIcon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon @click.stop="form.pcIcon = null" class="cloase-btn"
|
||||
name="widget-close" size="18" color="#fff" />
|
||||
<img :src="previewUrl(form.pcIcon)" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
<div class="bitmap-hint">矢量图标(上方)优先生效;此位图为存量回退。</div>
|
||||
</div>
|
||||
<div class="bitmap-upload-item">
|
||||
<div class="bitmap-label">App应用图标:</div>
|
||||
<Upload :on-success="(res: any) => onUploadSuccess(res, 'appIcon')"
|
||||
:before-upload="beforeBitmapUpload" :show-upload-list="false"
|
||||
:action="uploadUrl" :data="{ type: 'icon' }" accept=".svg,.png,.jpg,.jpeg"
|
||||
:with-credentials="true" :headers="{ yschapirequest: 1 }">
|
||||
<div v-if="!form.appIcon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon @click.stop="form.appIcon = null" class="cloase-btn"
|
||||
name="widget-close" size="18" color="#fff" />
|
||||
<img :src="previewUrl(form.appIcon)" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
<div class="bitmap-hint">存量移动端位图;矢量图标优先生效。</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -160,6 +198,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import AppIconPicker from "@/components/AppIconPicker.vue";
|
||||
import { computed, reactive, watch } from "vue";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
|
||||
@@ -205,6 +244,9 @@ const defaultForm = (): Record<string, any> => ({
|
||||
pcIcon: '',
|
||||
appUrl: '',
|
||||
appIcon: '',
|
||||
// 矢量图标(AppIcon 资产库):iconKey 空 = 不使用矢量,端侧走 关键词→位图→首字 原回退链
|
||||
iconKey: '',
|
||||
iconColor: '',
|
||||
officialAccountUrl: '',
|
||||
dingtalkUrl: '',
|
||||
});
|
||||
@@ -238,6 +280,17 @@ function onUploadSuccess(response: any, field: 'pcIcon' | 'appIcon') {
|
||||
}
|
||||
}
|
||||
|
||||
/* 位图上传前置校验:≤64KB(与后端 P4 校验同口径;svg 另由后端消毒) */
|
||||
const BITMAP_MAX_BYTES = 64 * 1024;
|
||||
|
||||
function beforeBitmapUpload(file: File) {
|
||||
if (file.size > BITMAP_MAX_BYTES) {
|
||||
Message.error(`图标文件 ${(file.size / 1024).toFixed(0)}KB 超过 64KB 上限,请压缩后上传`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---------- 前端类型选项 ---------- */
|
||||
// value 对齐后端平台枚举 com.yunhai.platform.utils.enums.ClientEnum(位掩码 key):
|
||||
// 1=Pc浏览器 2=移动App 8=微信公众号 32=钉钉;公众号/钉钉地址见表单第七行
|
||||
@@ -390,6 +443,27 @@ function onVisibleChange(val: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 位图上传(高级折叠区):并排两枚上传位 */
|
||||
.bitmap-upload-row {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.bitmap-upload-item {
|
||||
.bitmap-label {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bitmap-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 应用性质说明文字 */
|
||||
.nature-hint {
|
||||
margin-left: 8px;
|
||||
@@ -397,16 +471,21 @@ function onVisibleChange(val: boolean) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 表单帮助图标(Tooltip 触发器):默认浅灰,悬停点亮 */
|
||||
/* 表单帮助图标(Tooltip 触发器):静默态用中性灰阶令牌,交互态才升主色(含淡底),
|
||||
一律不写死色号——切档位(blue/ocean/ink/violet/teal)自动跟随。
|
||||
括号内兜底值 = 默认档 blue,仅防通道 CSS 不可达时图标失色(接入后由通道令牌接管)。 */
|
||||
.help-icon {
|
||||
margin-left: 6px;
|
||||
color: #c5c8ce;
|
||||
padding: 1px;
|
||||
border-radius: 50%;
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
transition: color 0.15s, background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: #2d8cf0;
|
||||
color: var(--brand, #577afb);
|
||||
background-color: var(--brand-soft, #eef2ff);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,7 +34,7 @@ const { form, loading: submitting, send: sendForm, onSuccess, onError } = useFor
|
||||
data: dto,
|
||||
params: props.schoolId != null ? { schoolId: props.schoolId } : undefined,
|
||||
}),
|
||||
{ initialForm: { id: null, name: '', categoryId: undefined, desc: '', appCode: '', appNature: 'BUSINESS', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||
{ initialForm: { id: null, name: '', categoryId: undefined, desc: '', appCode: '', appNature: 'BUSINESS', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', iconKey: '', iconColor: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||
);
|
||||
|
||||
onSuccess(() => {
|
||||
@@ -68,7 +68,8 @@ async function handleSubmit(formData: Record<string, any>) {
|
||||
if (!currentId.value) return;
|
||||
if (isSchoolMode) {
|
||||
// 学校模式:仅 名称/描述/Pc图标 三字段写学校覆盖表(后端 editAppForSchool 只读这三个);
|
||||
// categoryId/appCode/appNature 原样回传仅为满足平台级 Update 校验(后端不落平台表),缺失会 400
|
||||
// categoryId/appCode/appNature 原样回传仅为满足平台级 Update 校验(后端不落平台表),缺失会 400;
|
||||
// iconKey/iconColor 不送键(后端 null=不改——学校线不动平台矢量配置)
|
||||
form.value = {
|
||||
id: currentId.value,
|
||||
name: formData.name,
|
||||
@@ -91,6 +92,8 @@ async function handleSubmit(formData: Record<string, any>) {
|
||||
pcIcon: formData.pcIcon,
|
||||
appUrl: formData.appUrl,
|
||||
appIcon: formData.appIcon,
|
||||
iconKey: formData.iconKey,
|
||||
iconColor: formData.iconColor,
|
||||
officialAccountUrl: formData.officialAccountUrl,
|
||||
dingtalkUrl: formData.dingtalkUrl,
|
||||
};
|
||||
|
||||
+26
-9
@@ -99,15 +99,32 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||
server: {
|
||||
host: "localhost",
|
||||
port: 8130,
|
||||
// proxy: {
|
||||
// // //api是自行设置的请求前缀,任何请求路径以/api开头的请求将被代理到对应的target目标
|
||||
// // "/api": {
|
||||
// // target: "http://localhost:8064/",
|
||||
// // // target: "http://debugold.hongyucloud.com/ChildOptCourse",
|
||||
// // changeOrigin: true, //需要代理跨域
|
||||
// // rewrite: (path: any) => path.replace(/^\/api/, "/api/"), //路径重写,把'/api'替换为''
|
||||
// // },
|
||||
// },
|
||||
proxy: {
|
||||
// 主题通道本地联调(唯一必需项):dev 下 index.html 的 loader 走同源相对(见其注释的优先级四档),
|
||||
// manifest/CSS 经此代理到中央通道;changeOrigin 顺带把 Host 改写成目标域,Caddy 按 Host 匹配站点。
|
||||
// 联调本地通道源:YschTheme 侧 npm run serve:static(默认 9527),再用
|
||||
// YH_THEME_PROXY_TARGET=http://localhost:9527 覆盖本 target。prod 同源用不到这段。
|
||||
"/static": {
|
||||
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
|
||||
changeOrigin: true,
|
||||
},
|
||||
// 后端接口同源代理:alova 的 baseURL 在非 mock 时取 .env 的 VITE_SERVICE_HOST,
|
||||
// 想"只换后端不改 .env"就把 VITE_SERVICE_HOST 留空 → 请求落同源 /api、/mobiapi → 由此处代理决定去向。
|
||||
// 本地后端(默认):npm run dev
|
||||
// 线上后端: YH_API_PROXY_TARGET=http://base.yunhaixingtong.com/userbase npm run dev
|
||||
// (changeOrigin 已改写 Host,Caddy 按 Host 匹配;但 CAS 登录 cookie 属
|
||||
// yunhaixingtong 域,localhost 侧带不上 → 未登录接口会被 302 到 SSO,
|
||||
// 需要真数据时优先配合 VITE_IS_MOCK=false + 本地后端,或先在同浏览器登录线上)
|
||||
// mock 模式(VITE_IS_MOCK=true)下已注册的接口由 vite-plugin-mock 直接应答,未注册的仍透传到本 target。
|
||||
"/api": {
|
||||
target: process.env.YH_API_PROXY_TARGET || "http://localhost:8060",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/mobiapi": {
|
||||
target: process.env.YH_API_PROXY_TARGET || "http://localhost:8060",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// css预处理器
|
||||
css: {
|
||||
|
||||
Reference in New Issue
Block a user