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/域名、端口、密码和数据目录即可启动单镜像单容器环境。
|
想快速体验完整商城,不需要分别部署后端、PC、商家端、运营端、IM、H5、MySQL 和 Redis。安装 Docker Desktop 后,直接执行下面脚本,按提示输入 IP/域名、端口、密码和数据目录即可启动单镜像单容器环境。
|
||||||
|
|
||||||
|
数据库口径:常规部署和项目技术栈仍以 MySQL 为准;All-In-One 镜像内置 MariaDB 只是单容器本地体验中的 MySQL 协议兼容实现,不代表生产部署或常规部署已经切换为 MariaDB。
|
||||||
|
|
||||||
macOS / Linux:
|
macOS / Linux:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -26,7 +28,7 @@ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
|||||||
.\install-lilishop.ps1
|
.\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 私有化部署。
|
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ Lilishop 由 4 个独立仓库组成,均同步托管于 GitHub 与 Gitee:
|
|||||||
|
|
||||||
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP,降低开发和维护成本。
|
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP,降低开发和维护成本。
|
||||||
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
|
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
|
||||||
- **分布式架构**: 后端API服务化,支持独立部署和弹性伸缩。
|
- **All-In-One 后端**: 当前分支推荐使用 `lilishop-all` 统一后端入口,降低本地体验和私有化部署门槛。
|
||||||
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
|
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
|
||||||
- **容器化支持**: 提供Docker镜像和docker-compose配置,实现一键部署。
|
- **容器化支持**: 提供Docker镜像和docker-compose配置,实现一键部署。
|
||||||
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。
|
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ const localImBase = process.env.VUE_APP_IM_API_BASE_URL || localApiBase;
|
|||||||
|
|
||||||
// 开发环境优先读取一键启动脚本生成的 .env.development.local。
|
// 开发环境优先读取一键启动脚本生成的 .env.development.local。
|
||||||
const dev = {
|
const dev = {
|
||||||
im: localImBase || "https://im-api.pickmall.cn",
|
im: localImBase || "/api",
|
||||||
common: localApiBase || "https://common-api.pickmall.cn",
|
common: localApiBase || "/api",
|
||||||
buyer: localApiBase || "https://buyer-api.pickmall.cn",
|
buyer: localApiBase || "/api",
|
||||||
};
|
};
|
||||||
// 生产环境保持原有线上默认值,构建时也允许通过环境变量显式覆盖。
|
// 生产环境默认走 All-In-One 同域入口,构建时仍允许通过环境变量显式覆盖。
|
||||||
const prod = {
|
const prod = {
|
||||||
im: localImBase || "https://im-api.pickmall.cn",
|
im: localImBase || "/api",
|
||||||
common: localApiBase || "https://common-api.pickmall.cn",
|
common: localApiBase || "/api",
|
||||||
buyer: localApiBase || "https://buyer-api.pickmall.cn",
|
buyer: localApiBase || "/api",
|
||||||
};
|
};
|
||||||
|
|
||||||
//默认生产环境
|
//默认生产环境
|
||||||
|
|||||||
@@ -2,6 +2,19 @@ const name = "lilishop"; //全局商城name
|
|||||||
const schemeName = "lilishop"; //唤醒app需要的schemeName
|
const schemeName = "lilishop"; //唤醒app需要的schemeName
|
||||||
const wapUrl = process.env.VUE_APP_WAP_URL || "https://m-b2b2c.pickmall.cn";
|
const wapUrl = process.env.VUE_APP_WAP_URL || "https://m-b2b2c.pickmall.cn";
|
||||||
const loginCaptchaBypass = process.env.VUE_APP_LOGIN_CAPTCHA_BYPASS === "true";
|
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 {
|
export default {
|
||||||
name: name,
|
name: name,
|
||||||
@@ -16,7 +29,7 @@ export default {
|
|||||||
customerServiceMobile: "13161366885", //客服电话
|
customerServiceMobile: "13161366885", //客服电话
|
||||||
customerServiceEmail: "lili@lili.com", //客服邮箱
|
customerServiceEmail: "lili@lili.com", //客服邮箱
|
||||||
imWebSrc: process.env.VUE_APP_IM_WEB_URL || "https://im.pickmall.cn", //IM地址
|
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,
|
loginCaptchaBypass,
|
||||||
enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面
|
enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面
|
||||||
enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app
|
enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ page {
|
|||||||
.star {
|
.star {
|
||||||
width: 30rpx;
|
width: 30rpx;
|
||||||
height: 30rpx;
|
height: 30rpx;
|
||||||
background: url("/static/star.png");
|
background: url("../../static/star.png");
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.group-wrapper {
|
.group-wrapper {
|
||||||
background: url("/static/exchange.png");
|
background: url("../../../../static/exchange.png");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
.u-group-row {
|
.u-group-row {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.header-wraper {
|
.header-wraper {
|
||||||
background: url('/static/bg.png');
|
background: url('../../static/bg.png');
|
||||||
height: 200rpx;
|
height: 200rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default {
|
|||||||
.user-point {
|
.user-point {
|
||||||
padding: 0 20rpx;
|
padding: 0 20rpx;
|
||||||
height: 300rpx;
|
height: 300rpx;
|
||||||
background: url("/static/point-bg.png") no-repeat;
|
background: url("../../../static/point-bg.png") no-repeat;
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
}
|
}
|
||||||
.point {
|
.point {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-wraper {
|
.header-wraper {
|
||||||
background: url('/static/bg.png');
|
background: url('../../static/bg.png');
|
||||||
height: 200rpx;
|
height: 200rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
background-size: cover;
|
||||||
border-bottom-left-radius: 30rpx;
|
border-bottom-left-radius: 30rpx;
|
||||||
border-bottom-right-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-position: bottom;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
color: #ffffff;
|
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