mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-06-21 09:20:14 +08:00
Fix H5 all-in-one asset paths
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
|
||||
想快速体验完整商城,不需要分别部署后端、PC、商家端、运营端、IM、H5、MySQL 和 Redis。安装 Docker Desktop 后,直接执行下面脚本,按提示输入 IP/域名、端口、密码和数据目录即可启动单镜像单容器环境。
|
||||
|
||||
数据库口径:常规部署和项目技术栈仍以 MySQL 为准;All-In-One 镜像内置 MariaDB 只是单容器本地体验中的 MySQL 协议兼容实现,不代表生产部署或常规部署已经切换为 MariaDB。
|
||||
|
||||
macOS / Linux:
|
||||
|
||||
```bash
|
||||
@@ -26,7 +28,7 @@ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
.\install-lilishop.ps1
|
||||
```
|
||||
|
||||
默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5 和后端 API。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
|
||||
默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5,统一后端由 All-In-One 的同域 `/api/` 入口代理。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
|
||||
|
||||
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
|
||||
|
||||
@@ -80,7 +82,7 @@ Lilishop 由 4 个独立仓库组成,均同步托管于 GitHub 与 Gitee:
|
||||
|
||||
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP,降低开发和维护成本。
|
||||
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
|
||||
- **分布式架构**: 后端API服务化,支持独立部署和弹性伸缩。
|
||||
- **All-In-One 后端**: 当前分支推荐使用 `lilishop-all` 统一后端入口,降低本地体验和私有化部署门槛。
|
||||
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
|
||||
- **容器化支持**: 提供Docker镜像和docker-compose配置,实现一键部署。
|
||||
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。
|
||||
|
||||
@@ -7,15 +7,15 @@ const localImBase = process.env.VUE_APP_IM_API_BASE_URL || localApiBase;
|
||||
|
||||
// 开发环境优先读取一键启动脚本生成的 .env.development.local。
|
||||
const dev = {
|
||||
im: localImBase || "https://im-api.pickmall.cn",
|
||||
common: localApiBase || "https://common-api.pickmall.cn",
|
||||
buyer: localApiBase || "https://buyer-api.pickmall.cn",
|
||||
im: localImBase || "/api",
|
||||
common: localApiBase || "/api",
|
||||
buyer: localApiBase || "/api",
|
||||
};
|
||||
// 生产环境保持原有线上默认值,构建时也允许通过环境变量显式覆盖。
|
||||
// 生产环境默认走 All-In-One 同域入口,构建时仍允许通过环境变量显式覆盖。
|
||||
const prod = {
|
||||
im: localImBase || "https://im-api.pickmall.cn",
|
||||
common: localApiBase || "https://common-api.pickmall.cn",
|
||||
buyer: localApiBase || "https://buyer-api.pickmall.cn",
|
||||
im: localImBase || "/api",
|
||||
common: localApiBase || "/api",
|
||||
buyer: localApiBase || "/api",
|
||||
};
|
||||
|
||||
//默认生产环境
|
||||
|
||||
@@ -2,6 +2,19 @@ const name = "lilishop"; //全局商城name
|
||||
const schemeName = "lilishop"; //唤醒app需要的schemeName
|
||||
const wapUrl = process.env.VUE_APP_WAP_URL || "https://m-b2b2c.pickmall.cn";
|
||||
const loginCaptchaBypass = process.env.VUE_APP_LOGIN_CAPTCHA_BYPASS === "true";
|
||||
const normalizeWsUrl = (url) => {
|
||||
// H5 可使用同域相对路径,运行时按当前页面协议补齐 WebSocket 地址。
|
||||
if (!url || /^wss?:\/\//.test(url)) {
|
||||
return url || "";
|
||||
}
|
||||
// #ifdef H5
|
||||
if (typeof window !== "undefined" && url.startsWith("/")) {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}${url}`;
|
||||
}
|
||||
// #endif
|
||||
return url;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: name,
|
||||
@@ -16,7 +29,7 @@ export default {
|
||||
customerServiceMobile: "13161366885", //客服电话
|
||||
customerServiceEmail: "lili@lili.com", //客服邮箱
|
||||
imWebSrc: process.env.VUE_APP_IM_WEB_URL || "https://im.pickmall.cn", //IM地址
|
||||
baseWsUrl: process.env.VUE_APP_IM_WS_URL || "wss://im-api.pickmall.cn/lili/webSocket", // IM WS 地址
|
||||
baseWsUrl: normalizeWsUrl(process.env.VUE_APP_IM_WS_URL || "/lili/webSocket"), // IM WS 地址
|
||||
loginCaptchaBypass,
|
||||
enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面
|
||||
enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app
|
||||
|
||||
@@ -321,7 +321,7 @@ page {
|
||||
.star {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
background: url("/static/star.png");
|
||||
background: url("../../static/star.png");
|
||||
background-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.group-wrapper {
|
||||
background: url("/static/exchange.png");
|
||||
background: url("../../../../static/exchange.png");
|
||||
background-size: cover;
|
||||
}
|
||||
.u-group-row {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-wraper {
|
||||
background: url('/static/bg.png');
|
||||
background: url('../../static/bg.png');
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
.user-point {
|
||||
padding: 0 20rpx;
|
||||
height: 300rpx;
|
||||
background: url("/static/point-bg.png") no-repeat;
|
||||
background: url("../../../static/point-bg.png") no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
.point {
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
}
|
||||
|
||||
.header-wraper {
|
||||
background: url('/static/bg.png');
|
||||
background: url('../../static/bg.png');
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
10
pages/tabbar/user/my.contract.test.mjs
Normal file
10
pages/tabbar/user/my.contract.test.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = readFileSync(new URL('./my.vue', import.meta.url), 'utf8');
|
||||
|
||||
test('member center header background resolves under the H5 base path', () => {
|
||||
assert.doesNotMatch(source, /background-image:\s*url\(["']\/static\/img\/main-bg\.png["']\)/);
|
||||
assert.match(source, /background-image:\s*url\(["']\.\.\/\.\.\/\.\.\/static\/img\/main-bg\.png["']\)/);
|
||||
});
|
||||
@@ -174,7 +174,7 @@ body {
|
||||
background-size: cover;
|
||||
border-bottom-left-radius: 30rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
background-image: url("/static/img/main-bg.png");
|
||||
background-image: url("../../../static/img/main-bg.png");
|
||||
background-position: bottom;
|
||||
background-repeat: no-repeat;
|
||||
color: #ffffff;
|
||||
|
||||
143
static-asset-url.contract.test.mjs
Normal file
143
static-asset-url.contract.test.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import valueParser from 'postcss-value-parser';
|
||||
import vueTemplateCompiler from 'vue-template-compiler';
|
||||
|
||||
const roots = ['pages', 'components'];
|
||||
const extraFiles = ['App.vue', 'uni.scss'];
|
||||
const styleExtensions = new Set(['.css', '.scss']);
|
||||
|
||||
function listFiles(dir) {
|
||||
const result = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const fullPath = path.join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
result.push(...listFiles(fullPath));
|
||||
} else {
|
||||
result.push(fullPath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stripCssCommentsPreserveOffsets(source) {
|
||||
let output = '';
|
||||
let quote = null;
|
||||
|
||||
for (let i = 0; i < source.length; i += 1) {
|
||||
const char = source[i];
|
||||
const next = source[i + 1];
|
||||
|
||||
if (quote) {
|
||||
output += char;
|
||||
if (char === '\\') {
|
||||
i += 1;
|
||||
output += source[i] || '';
|
||||
} else if (char === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
output += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '/' && next === '*') {
|
||||
output += ' ';
|
||||
i += 2;
|
||||
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
||||
output += source[i] === '\n' ? '\n' : ' ';
|
||||
i += 1;
|
||||
}
|
||||
output += ' ';
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '/' && next === '/') {
|
||||
output += ' ';
|
||||
i += 2;
|
||||
while (i < source.length && source[i] !== '\n') {
|
||||
output += ' ';
|
||||
i += 1;
|
||||
}
|
||||
if (source[i] === '\n') {
|
||||
output += '\n';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
output += char;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function lineForOffset(source, offset) {
|
||||
return source.slice(0, offset).split('\n').length;
|
||||
}
|
||||
|
||||
function collectRootStaticStyleUrls(filePath, styleSource, sourceOffset = 0, originalSource = styleSource) {
|
||||
const stripped = stripCssCommentsPreserveOffsets(styleSource);
|
||||
const findings = [];
|
||||
|
||||
valueParser(stripped).walk((node) => {
|
||||
if (node.type !== 'function' || node.value.toLowerCase() !== 'url') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = valueParser.stringify(node.nodes).trim().replace(/^['"]|['"]$/g, '');
|
||||
if (!url.startsWith('/static/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
findings.push({
|
||||
filePath,
|
||||
line: lineForOffset(originalSource, sourceOffset + node.sourceIndex),
|
||||
url,
|
||||
});
|
||||
});
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function styleSourcesForFile(filePath) {
|
||||
const source = readFileSync(filePath, 'utf8');
|
||||
if (filePath.endsWith('.vue')) {
|
||||
const descriptor = vueTemplateCompiler.parseComponent(source);
|
||||
return descriptor.styles.map((style) => ({
|
||||
source: style.content,
|
||||
sourceOffset: style.start,
|
||||
originalSource: source,
|
||||
}));
|
||||
}
|
||||
|
||||
return [{ source, sourceOffset: 0, originalSource: source }];
|
||||
}
|
||||
|
||||
test('style url() references do not use H5-root /static paths', () => {
|
||||
const files = [
|
||||
...roots.flatMap((root) => listFiles(root)),
|
||||
...extraFiles,
|
||||
].filter((filePath) => filePath.endsWith('.vue') || styleExtensions.has(path.extname(filePath)));
|
||||
|
||||
const findings = files.flatMap((filePath) =>
|
||||
styleSourcesForFile(filePath).flatMap(({ source, sourceOffset, originalSource }) =>
|
||||
collectRootStaticStyleUrls(filePath, source, sourceOffset, originalSource)
|
||||
)
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
findings,
|
||||
[],
|
||||
`style url() must use relative static paths so H5 /h5 deployments do not request site-root assets:\n${
|
||||
findings.map((item) => `${item.filePath}:${item.line} ${item.url}`).join('\n')
|
||||
}`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user