Compare commits

...

8 Commits

Author SHA1 Message Date
icssoa
e66d64d83e clean 判断错误 2025-08-30 14:52:23 +08:00
icssoa
9330acc33c uniappx eps 类型处理 2025-08-26 18:41:52 +08:00
icssoa
e125957ef5 更新 uniappx 语言插件 2025-08-26 09:54:52 +08:00
icssoa
798ecc7304 添加 unix 纯净版模式 2025-08-11 16:32:41 +08:00
icssoa
83360678c2 Merge branch '8.x' of https://e.coding.net/shancool/cool-admin/front-next into 8.x 2025-08-11 14:42:27 +08:00
icssoa
928848877b 优化 2025-08-11 14:42:26 +08:00
icssoa
09d4697629 添加获取颜色值 2025-07-26 17:39:45 +08:00
icssoa
79fd3b9093 优化 2025-07-25 11:11:18 +08:00
13 changed files with 988 additions and 776 deletions

View File

@ -31,4 +31,5 @@ export declare const config: {
rpxRatio: number; rpxRatio: number;
darkTextClass: string; darkTextClass: string;
}; };
clean: boolean;
}; };

View File

@ -56,6 +56,7 @@
rpxRatio: 2, rpxRatio: 2,
darkTextClass: "dark:text-surface-50", darkTextClass: "dark:text-surface-50",
}, },
clean: false,
}; };
// 根目录 // 根目录
@ -92,13 +93,69 @@
function readFile(path, json) { function readFile(path, json) {
try { try {
const content = fs.readFileSync(path, "utf8"); const content = fs.readFileSync(path, "utf8");
return json return json ? JSON.parse(removeJsonComments(content)) : content;
? JSON.parse(content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
: content;
} }
catch (err) { } catch (err) { }
return ""; return "";
} }
// 安全地移除JSON中的注释
function removeJsonComments(content) {
let result = "";
let inString = false;
let stringChar = "";
let escaped = false;
let i = 0;
while (i < content.length) {
const char = content[i];
const nextChar = content[i + 1];
// 处理字符串状态
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
result += char;
}
else if (inString && char === stringChar && !escaped) {
inString = false;
stringChar = "";
result += char;
}
else if (inString) {
// 在字符串内,直接添加字符
result += char;
escaped = char === "\\" && !escaped;
}
else {
// 不在字符串内,检查注释
if (char === "/" && nextChar === "/") {
// 单行注释,跳过到行尾
while (i < content.length && content[i] !== "\n") {
i++;
}
if (i < content.length) {
result += content[i]; // 保留换行符
}
}
else if (char === "/" && nextChar === "*") {
// 多行注释,跳过到 */
i += 2;
while (i < content.length - 1) {
if (content[i] === "*" && content[i + 1] === "/") {
i += 2;
break;
}
i++;
}
continue;
}
else {
result += char;
escaped = false;
}
}
i++;
}
return result;
}
// 写入文件 // 写入文件
function writeFile(path, data) { function writeFile(path, data) {
try { try {
@ -138,6 +195,26 @@
function error(message) { function error(message) {
console.log("\x1B[31m%s\x1B[0m", message); console.log("\x1B[31m%s\x1B[0m", message);
} }
/**
* 比较两个版本号
* @param version1 版本号1 (: "1.2.3")
* @param version2 版本号2 (: "1.2.4")
* @returns 1: version1 > version2, 0: 相等, -1: version1 < version2
*/
function compareVersion(version1, version2) {
const v1Parts = version1.split(".").map(Number);
const v2Parts = version2.split(".").map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part)
return 1;
if (v1Part < v2Part)
return -1;
}
return 0;
}
/** /**
* 将模板字符串扁平化处理转换为 Service 类型定义 * 将模板字符串扁平化处理转换为 Service 类型定义
@ -151,15 +228,25 @@
// 保留 Service 类型定义前的内容 // 保留 Service 类型定义前的内容
let header = template.substring(0, startIndex); let header = template.substring(0, startIndex);
// 获取 Service 类型定义及其内容,去除换行和制表符 // 获取 Service 类型定义及其内容,去除换行和制表符
const serviceContent = template.substring(startIndex).replace(/\n|\t/g, ""); const serviceTemplateContent = template.substring(startIndex).replace(/\n|\t/g, "");
// 找到 Service 的内容部分
const serviceStartIndex = serviceTemplateContent.indexOf("{") + 1;
const serviceEndIndex = findClosingBrace(serviceTemplateContent, serviceStartIndex);
const serviceInnerContent = serviceTemplateContent
.substring(serviceStartIndex, serviceEndIndex)
.trim();
// 存储所有接口定义
const allInterfaces = new Map();
// 处理 Service 内容,保持原有结构但替换嵌套对象为接口引用
const serviceContent = buildCurrentLevelContent(serviceInnerContent);
// 递归收集所有需要生成的接口
flattenContent(serviceInnerContent, allInterfaces);
// 生成所有接口定义
let interfaces = ""; let interfaces = "";
let serviceFields = ""; allInterfaces.forEach((content, key) => {
// 解析内容并生成接口定义 interfaces += `\nexport interface ${firstUpperCase(key)}Interface { ${content} }\n`;
parse(serviceContent).forEach(({ key, content, level }) => {
interfaces += `\nexport interface ${firstUpperCase(key)}Interface {${content}}\n`;
serviceFields += `${key}: ${firstUpperCase(key)}Interface;`;
}); });
return `${header}${interfaces}\nexport type Service = {${serviceFields}}`; return `${header}${interfaces}\nexport type Service = { ${serviceContent} }`;
} }
/** /**
* 查找匹配的右花括号位置 * 查找匹配的右花括号位置
@ -184,40 +271,51 @@
return currentIndex - 1; return currentIndex - 1;
} }
/** /**
* 解析内容中的嵌套结构 * 递归收集所有需要生成的接口
* @param content - 要解析的内容字符串 * @param content - 要处理的内容
* @returns 解析结果数组包含解析出的键值对 * @param allInterfaces - 存储所有接口定义的 Map
* @param parentFields - 父级字段数组暂未使用
*/ */
function parse(content, level = 0) { function flattenContent(content, allInterfaces, parentFields) {
// 匹配形如 xxx: { ... } 的结构
const interfacePattern = /(\w+)\s*:\s*\{/g; const interfacePattern = /(\w+)\s*:\s*\{/g;
const result = [];
let match; let match;
while ((match = interfacePattern.exec(content)) !== null) { while ((match = interfacePattern.exec(content)) !== null) {
const key = match[1];
const startIndex = match.index + match[0].length; const startIndex = match.index + match[0].length;
const endIndex = findClosingBrace(content, startIndex); const endIndex = findClosingBrace(content, startIndex);
if (endIndex > startIndex) { if (endIndex > startIndex) {
let parsedContent = content.substring(startIndex, endIndex).trim(); const innerContent = content.substring(startIndex, endIndex).trim();
// 处理嵌套结构 // 构建当前接口的内容,将嵌套对象替换为接口引用
if (parsedContent.includes("{") && parsedContent.includes("}")) { const currentLevelContent = buildCurrentLevelContent(innerContent);
const nestedInterfaces = parse(parsedContent, level + 1); allInterfaces.set(key, currentLevelContent);
// 替换嵌套的内容为接口引用 // 递归处理嵌套内容
if (nestedInterfaces.length > 0) { flattenContent(innerContent, allInterfaces);
nestedInterfaces.forEach((nestedInterface) => {
const pattern = `${nestedInterface.key}: {${nestedInterface.content}};`;
const replacement = `${nestedInterface.key}: ${firstUpperCase(nestedInterface.key)}Interface`;
parsedContent = parsedContent.replace(pattern, replacement);
});
}
}
// 将解析结果添加到数组开头
result.unshift({
key: match[1],
level,
content: parsedContent,
});
} }
} }
}
/**
* 构建当前级别的内容将嵌套对象替换为接口引用
* @param content - 内容字符串
* @returns 处理后的内容
*/
function buildCurrentLevelContent(content) {
const interfacePattern = /(\w+)\s*:\s*\{/g;
let result = content;
let match;
// 重置正则表达式的 lastIndex
interfacePattern.lastIndex = 0;
while ((match = interfacePattern.exec(content)) !== null) {
const key = match[1];
const startIndex = match.index + match[0].length;
const endIndex = findClosingBrace(content, startIndex);
if (endIndex > startIndex) {
const fullMatch = content.substring(match.index, endIndex + 1);
const replacement = `${key}: ${firstUpperCase(key)}Interface;`;
result = result.replace(fullMatch, replacement);
}
}
// 清理多余的分号和空格
result = result.replace(/;+/g, ";").replace(/\s+/g, " ").trim();
return result; return result;
} }
@ -797,8 +895,9 @@
printWidth: 100, printWidth: 100,
trailingComma: "none", trailingComma: "none",
}) })
.catch(() => { .catch((err) => {
error(`[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`); console.log(err);
error(`[cool-eps] File format error, please try again`);
return null; return null;
}); });
} }
@ -846,7 +945,7 @@
} }
}); });
if (config.type == "uniapp-x" || config.type == "app") { if (config.type == "uniapp-x" || config.type == "app") {
list = list.filter((e) => e.prefix.startsWith("/app")); list = list.filter((e) => e.prefix.startsWith("/app") || e.prefix.startsWith("/admin"));
} }
} }
/** /**
@ -854,22 +953,25 @@
* @returns {boolean} 是否有更新 * @returns {boolean} 是否有更新
*/ */
function createJson() { function createJson() {
if (config.type == "uniapp-x") { let data = [];
return false; if (config.type != "uniapp-x") {
data = list.map((e) => {
return {
prefix: e.prefix,
name: e.name || "",
api: e.api.map((apiItem) => ({
name: apiItem.name,
method: apiItem.method,
path: apiItem.path,
})),
search: e.search,
};
});
} }
const arr = list.map((e) => { else {
return { data = list;
prefix: e.prefix, }
name: e.name || "", const content = JSON.stringify(data);
api: e.api.map((apiItem) => ({
name: apiItem.name,
method: apiItem.method,
path: apiItem.path,
})),
search: e.search,
};
});
const content = JSON.stringify(arr);
const local_content = readFile(getEpsPath("eps.json")); const local_content = readFile(getEpsPath("eps.json"));
// 判断是否需要更新 // 判断是否需要更新
const isUpdate = content != local_content; const isUpdate = content != local_content;
@ -894,6 +996,9 @@
for (const item of list) { for (const item of list) {
if (!checkName(item.name)) if (!checkName(item.name))
continue; continue;
if (formatName(item.name) == "BusinessInterface") {
console.log(111);
}
let t = `interface ${formatName(item.name)} {`; let t = `interface ${formatName(item.name)} {`;
// 合并 columns 和 pageColumns去重 // 合并 columns 和 pageColumns去重
const columns = lodash.uniqBy(lodash.compact([...(item.columns || []), ...(item.pageColumns || [])]), "source"); const columns = lodash.uniqBy(lodash.compact([...(item.columns || []), ...(item.pageColumns || [])]), "source");
@ -1007,12 +1112,22 @@
break; break;
} }
// 方法描述 // 方法描述
t += ` if (config.type == "uniapp-x") {
/** t += `
* ${a.summary || n} /**
*/ * ${a.summary || n}
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>; */
`; ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<any>;
`;
}
else {
t += `
/**
* ${a.summary || n}
*/
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
`;
}
if (!permission.includes(n)) { if (!permission.includes(n)) {
permission.push(n); permission.push(n);
} }
@ -1054,6 +1169,8 @@
return ` return `
type json = any; type json = any;
${await createDict()}
interface PagePagination { interface PagePagination {
size: number; size: number;
page: number; page: number;
@ -1083,8 +1200,6 @@
${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")} ${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")}
${await createDict()}
type Service = { type Service = {
${noUniappX("request: Request;")} ${noUniappX("request: Request;")}
@ -1249,32 +1364,13 @@
if (item.name) { if (item.name) {
types.push(item.name); types.push(item.name);
} }
// 返回类型
let res = "";
// 实体名
const en = item.name || "any";
switch (a.path) {
case "/page":
res = `${name}PageResponse`;
types.push(res);
break;
case "/list":
res = `${en}[]`;
break;
case "/info":
res = en;
break;
default:
res = "any";
break;
}
// 方法描述 // 方法描述
t += ` t += `
/** /**
* ${a.summary || n} * ${a.summary || n}
*/ */
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")})${noUniappX(`: Promise<${res}>`)} { ${n}(data?: any): Promise<any> {
return request<${res}>({ return request({
url: "/${d[i].namespace}${a.path}", url: "/${d[i].namespace}${a.path}",
method: "${(a.method || "get").toLocaleUpperCase()}", method: "${(a.method || "get").toLocaleUpperCase()}",
data, data,
@ -2176,6 +2272,70 @@ if (typeof window !== 'undefined') {
return [postcssPlugin(), transformPlugin()]; return [postcssPlugin(), transformPlugin()];
} }
// 获取 tailwind.config.ts 中的颜色
function getTailwindColor() {
const config = readFile(rootDir("tailwind.config.ts"));
if (!config) {
return null;
}
try {
// 从配置文件中动态提取主色和表面色
const colorResult = {};
// 提取 getPrimary 调用中的颜色名称
const primaryMatch = config.match(/getPrimary\(["']([^"']+)["']\)/);
const primaryColorName = primaryMatch?.[1];
// 提取 getSurface 调用中的颜色名称
const surfaceMatch = config.match(/getSurface\(["']([^"']+)["']\)/);
const surfaceColorName = surfaceMatch?.[1];
if (primaryColorName) {
// 提取 PRIMARY_COLOR_PALETTES 中对应的调色板
const primaryPaletteMatch = config.match(new RegExp(`{\\s*name:\\s*["']${primaryColorName}["'],\\s*palette:\\s*({[^}]+})`, "s"));
if (primaryPaletteMatch) {
// 解析调色板对象
const paletteStr = primaryPaletteMatch[1];
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
if (paletteEntries) {
paletteEntries.forEach((entry) => {
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
if (match) {
const [, key, value] = match;
colorResult[`primary-${key}`] = value;
}
});
}
}
}
if (surfaceColorName) {
// 提取 SURFACE_PALETTES 中对应的调色板
const surfacePaletteMatch = config.match(new RegExp(`{\\s*name:\\s*["']${surfaceColorName}["'],\\s*palette:\\s*({[^}]+})`, "s"));
if (surfacePaletteMatch) {
// 解析调色板对象
const paletteStr = surfacePaletteMatch[1];
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
if (paletteEntries) {
paletteEntries.forEach((entry) => {
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
if (match) {
const [, key, value] = match;
// 0 对应 surface其他对应 surface-*
const colorKey = key === "0" ? "surface" : `surface-${key}`;
colorResult[colorKey] = value;
}
});
}
}
}
return colorResult;
}
catch (error) {
return null;
}
}
// 获取版本号
function getVersion() {
const pkg = readFile(rootDir("package.json"), true);
return pkg?.version || "0.0.0";
}
function codePlugin() { function codePlugin() {
return [ return [
{ {
@ -2184,25 +2344,34 @@ if (typeof window !== 'undefined') {
async transform(code, id) { async transform(code, id) {
if (id.includes("/cool/ctx/index.ts")) { if (id.includes("/cool/ctx/index.ts")) {
const ctx = await createCtx(); const ctx = await createCtx();
const theme = await readFile(rootDir("theme.json"), true); // 版本
const version = getVersion();
// 主题配置
const theme = readFile(rootDir("theme.json"), true);
// 主题配置
ctx["theme"] = theme;
if (compareVersion(version, "8.0.2") >= 0) {
// 颜色值
ctx["color"] = getTailwindColor();
}
// 安全字符映射
ctx["SAFE_CHAR_MAP_LOCALE"] = []; ctx["SAFE_CHAR_MAP_LOCALE"] = [];
for (const i in SAFE_CHAR_MAP_LOCALE) { for (const i in SAFE_CHAR_MAP_LOCALE) {
ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]); ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]);
} }
ctx["theme"] = theme;
code = code.replace("const ctx = {}", `const ctx = ${JSON.stringify(ctx, null, 4)}`); code = code.replace("const ctx = {}", `const ctx = ${JSON.stringify(ctx, null, 4)}`);
} }
if (id.includes("/cool/service/index.ts")) { // if (id.includes("/cool/service/index.ts")) {
const eps = await createEps(); // const eps = await createEps();
if (eps.serviceCode) { // if (eps.serviceCode) {
const { content, types } = eps.serviceCode; // const { content, types } = eps.serviceCode;
const typeCode = `import type { ${lodash.uniq(types).join(", ")} } from '../types';`; // const typeCode = `import type { ${uniq(types).join(", ")} } from '../types';`;
code = // code =
typeCode + // typeCode +
"\n\n" + // "\n\n" +
code.replace("const service = {}", `const service = ${content}`); // code.replace("const service = {}", `const service = ${content}`);
} // }
} // }
if (id.endsWith(".json")) { if (id.endsWith(".json")) {
const d = JSON.parse(code); const d = JSON.parse(code);
for (let i in d) { for (let i in d) {
@ -2215,7 +2384,17 @@ if (typeof window !== 'undefined') {
delete d[i]; delete d[i];
} }
} }
code = JSON.stringify(d); // 转字符串不然会报错Method too large
if (id.includes("/locale/")) {
let t = [];
d.forEach(([a, b]) => {
t.push(`${a}<__=__>${b}`);
});
code = JSON.stringify([[t.join("<__&__>")]]);
}
else {
code = JSON.stringify(d);
}
} }
return { return {
code, code,
@ -2258,6 +2437,14 @@ if (typeof window !== 'undefined') {
config.type = options.type; config.type = options.type;
// 请求地址 // 请求地址
config.reqUrl = getProxyTarget(options.proxy); config.reqUrl = getProxyTarget(options.proxy);
if (config.type == "uniapp-x") {
// 是否纯净版
config.clean = options.clean ?? true;
if (config.clean) {
// 默认设置为测试地址
config.reqUrl = "https://show.cool-admin.com/api";
}
}
// 是否开启名称标签 // 是否开启名称标签
config.nameTag = options.nameTag ?? true; config.nameTag = options.nameTag ?? true;
// svg // svg
@ -2282,6 +2469,10 @@ if (typeof window !== 'undefined') {
lodash.merge(config.eps.mapping, mapping); lodash.merge(config.eps.mapping, mapping);
} }
} }
// 如果类型为 uniapp-x则关闭 eps
if (config.type == "uniapp-x") {
config.eps.enable = false;
}
// tailwind // tailwind
if (options.tailwind) { if (options.tailwind) {
lodash.assign(config.tailwind, options.tailwind); lodash.assign(config.tailwind, options.tailwind);

View File

@ -9,3 +9,10 @@ export declare function parseJson(req: any): Promise<any>;
export declare function formatContent(content: string, options?: prettier.Options): Promise<string>; export declare function formatContent(content: string, options?: prettier.Options): Promise<string>;
export declare function error(message: string): void; export declare function error(message: string): void;
export declare function success(message: string): void; export declare function success(message: string): void;
/**
*
* @param version1 1 (: "1.2.3")
* @param version2 2 (: "1.2.4")
* @returns 1: version1 > version2, 0: 相等, -1: version1 < version2
*/
export declare function compareVersion(version1: string, version2: string): number;

View File

@ -1,6 +1,6 @@
{ {
"name": "@cool-vue/vite-plugin", "name": "@cool-vue/vite-plugin",
"version": "8.2.3", "version": "8.2.10",
"description": "cool-admin、cool-uni builder", "description": "cool-admin、cool-uni builder",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"main": "/dist/index.js", "main": "/dist/index.js",

View File

@ -53,4 +53,5 @@ export const config = {
rpxRatio: 2, rpxRatio: 2,
darkTextClass: "dark:text-surface-50", darkTextClass: "dark:text-surface-50",
}, },
clean: false,
}; };

View File

@ -130,10 +130,9 @@ async function formatCode(text: string): Promise<string | null> {
printWidth: 100, printWidth: 100,
trailingComma: "none", trailingComma: "none",
}) })
.catch(() => { .catch((err) => {
error( console.log(err);
`[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`, error(`[cool-eps] File format error, please try again`);
);
return null; return null;
}); });
} }
@ -182,7 +181,7 @@ async function getData() {
}); });
if (config.type == "uniapp-x" || config.type == "app") { if (config.type == "uniapp-x" || config.type == "app") {
list = list.filter((e) => e.prefix.startsWith("/app")); list = list.filter((e) => e.prefix.startsWith("/app") || e.prefix.startsWith("/admin"));
} }
} }
@ -191,24 +190,26 @@ async function getData() {
* @returns {boolean} * @returns {boolean}
*/ */
function createJson(): boolean { function createJson(): boolean {
if (config.type == "uniapp-x") { let data: any[] = [];
return false;
if (config.type != "uniapp-x") {
data = list.map((e) => {
return {
prefix: e.prefix,
name: e.name || "",
api: e.api.map((apiItem) => ({
name: apiItem.name,
method: apiItem.method,
path: apiItem.path,
})),
search: e.search,
};
});
} else {
data = list;
} }
const arr = list.map((e) => { const content = JSON.stringify(data);
return {
prefix: e.prefix,
name: e.name || "",
api: e.api.map((apiItem) => ({
name: apiItem.name,
method: apiItem.method,
path: apiItem.path,
})),
search: e.search,
};
});
const content = JSON.stringify(arr);
const local_content = readFile(getEpsPath("eps.json")); const local_content = readFile(getEpsPath("eps.json"));
// 判断是否需要更新 // 判断是否需要更新
@ -238,6 +239,10 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
for (const item of list) { for (const item of list) {
if (!checkName(item.name)) continue; if (!checkName(item.name)) continue;
if (formatName(item.name) == "BusinessInterface") {
console.log(111);
}
let t = `interface ${formatName(item.name)} {`; let t = `interface ${formatName(item.name)} {`;
// 合并 columns 和 pageColumns去重 // 合并 columns 和 pageColumns去重
@ -377,12 +382,21 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
} }
// 方法描述 // 方法描述
t += ` if (config.type == "uniapp-x") {
/** t += `
* ${a.summary || n} /**
*/ * ${a.summary || n}
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>; */
`; ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<any>;
`;
} else {
t += `
/**
* ${a.summary || n}
*/
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
`;
}
if (!permission.includes(n)) { if (!permission.includes(n)) {
permission.push(n); permission.push(n);
@ -431,6 +445,8 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
return ` return `
type json = any; type json = any;
${await createDict()}
interface PagePagination { interface PagePagination {
size: number; size: number;
page: number; page: number;
@ -460,8 +476,6 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")} ${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")}
${await createDict()}
type Service = { type Service = {
${noUniappX("request: Request;")} ${noUniappX("request: Request;")}
@ -656,35 +670,13 @@ function createServiceCode(): { content: string; types: string[] } {
types.push(item.name); types.push(item.name);
} }
// 返回类型
let res = "";
// 实体名
const en = item.name || "any";
switch (a.path) {
case "/page":
res = `${name}PageResponse`;
types.push(res);
break;
case "/list":
res = `${en}[]`;
break;
case "/info":
res = en;
break;
default:
res = "any";
break;
}
// 方法描述 // 方法描述
t += ` t += `
/** /**
* ${a.summary || n} * ${a.summary || n}
*/ */
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")})${noUniappX(`: Promise<${res}>`)} { ${n}(data?: any): Promise<any> {
return request<${res}>({ return request({
url: "/${d[i].namespace}${a.path}", url: "/${d[i].namespace}${a.path}",
method: "${(a.method || "get").toLocaleUpperCase()}", method: "${(a.method || "get").toLocaleUpperCase()}",
data, data,

View File

@ -14,6 +14,16 @@ export function cool(options: Config.Options) {
// 请求地址 // 请求地址
config.reqUrl = getProxyTarget(options.proxy); config.reqUrl = getProxyTarget(options.proxy);
if (config.type == "uniapp-x") {
// 是否纯净版
config.clean = options.clean ?? true;
if (config.clean) {
// 默认设置为测试地址
config.reqUrl = "https://show.cool-admin.com/api";
}
}
// 是否开启名称标签 // 是否开启名称标签
config.nameTag = options.nameTag ?? true; config.nameTag = options.nameTag ?? true;
@ -45,6 +55,11 @@ export function cool(options: Config.Options) {
} }
} }
// 如果类型为 uniapp-x则关闭 eps
if (config.type == "uniapp-x") {
config.eps.enable = false;
}
// tailwind // tailwind
if (options.tailwind) { if (options.tailwind) {
assign(config.tailwind, options.tailwind); assign(config.tailwind, options.tailwind);

View File

@ -1,10 +1,96 @@
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { SAFE_CHAR_MAP_LOCALE } from "./config"; import { SAFE_CHAR_MAP_LOCALE } from "./config";
import { createCtx } from "../ctx"; import { createCtx } from "../ctx";
import { readFile, rootDir } from "../utils"; import { compareVersion, readFile, rootDir } from "../utils";
import { createEps } from "../eps"; import { createEps } from "../eps";
import { uniq } from "lodash"; import { uniq } from "lodash";
// 获取 tailwind.config.ts 中的颜色
function getTailwindColor() {
const config = readFile(rootDir("tailwind.config.ts"));
if (!config) {
return null;
}
try {
// 从配置文件中动态提取主色和表面色
const colorResult: Record<string, string> = {};
// 提取 getPrimary 调用中的颜色名称
const primaryMatch = config.match(/getPrimary\(["']([^"']+)["']\)/);
const primaryColorName = primaryMatch?.[1];
// 提取 getSurface 调用中的颜色名称
const surfaceMatch = config.match(/getSurface\(["']([^"']+)["']\)/);
const surfaceColorName = surfaceMatch?.[1];
if (primaryColorName) {
// 提取 PRIMARY_COLOR_PALETTES 中对应的调色板
const primaryPaletteMatch = config.match(
new RegExp(
`{\\s*name:\\s*["']${primaryColorName}["'],\\s*palette:\\s*({[^}]+})`,
"s",
),
);
if (primaryPaletteMatch) {
// 解析调色板对象
const paletteStr = primaryPaletteMatch[1];
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
if (paletteEntries) {
paletteEntries.forEach((entry: string) => {
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
if (match) {
const [, key, value] = match;
colorResult[`primary-${key}`] = value;
}
});
}
}
}
if (surfaceColorName) {
// 提取 SURFACE_PALETTES 中对应的调色板
const surfacePaletteMatch = config.match(
new RegExp(
`{\\s*name:\\s*["']${surfaceColorName}["'],\\s*palette:\\s*({[^}]+})`,
"s",
),
);
if (surfacePaletteMatch) {
// 解析调色板对象
const paletteStr = surfacePaletteMatch[1];
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
if (paletteEntries) {
paletteEntries.forEach((entry: string) => {
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
if (match) {
const [, key, value] = match;
// 0 对应 surface其他对应 surface-*
const colorKey = key === "0" ? "surface" : `surface-${key}`;
colorResult[colorKey] = value;
}
});
}
}
}
return colorResult;
} catch (error) {
return null;
}
}
// 获取版本号
function getVersion() {
const pkg = readFile(rootDir("package.json"), true);
return pkg?.version || "0.0.0";
}
export function codePlugin(): Plugin[] { export function codePlugin(): Plugin[] {
return [ return [
{ {
@ -13,34 +99,46 @@ export function codePlugin(): Plugin[] {
async transform(code, id) { async transform(code, id) {
if (id.includes("/cool/ctx/index.ts")) { if (id.includes("/cool/ctx/index.ts")) {
const ctx = await createCtx(); const ctx = await createCtx();
const theme = await readFile(rootDir("theme.json"), true);
// 版本
const version = getVersion();
// 主题配置
const theme = readFile(rootDir("theme.json"), true);
// 主题配置
ctx["theme"] = theme;
if (compareVersion(version, "8.0.2") >= 0) {
// 颜色值
ctx["color"] = getTailwindColor();
}
// 安全字符映射
ctx["SAFE_CHAR_MAP_LOCALE"] = []; ctx["SAFE_CHAR_MAP_LOCALE"] = [];
for (const i in SAFE_CHAR_MAP_LOCALE) { for (const i in SAFE_CHAR_MAP_LOCALE) {
ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]); ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]);
} }
ctx["theme"] = theme;
code = code.replace( code = code.replace(
"const ctx = {}", "const ctx = {}",
`const ctx = ${JSON.stringify(ctx, null, 4)}`, `const ctx = ${JSON.stringify(ctx, null, 4)}`,
); );
} }
if (id.includes("/cool/service/index.ts")) { // if (id.includes("/cool/service/index.ts")) {
const eps = await createEps(); // const eps = await createEps();
if (eps.serviceCode) { // if (eps.serviceCode) {
const { content, types } = eps.serviceCode; // const { content, types } = eps.serviceCode;
const typeCode = `import type { ${uniq(types).join(", ")} } from '../types';`; // const typeCode = `import type { ${uniq(types).join(", ")} } from '../types';`;
code = // code =
typeCode + // typeCode +
"\n\n" + // "\n\n" +
code.replace("const service = {}", `const service = ${content}`); // code.replace("const service = {}", `const service = ${content}`);
} // }
} // }
if (id.endsWith(".json")) { if (id.endsWith(".json")) {
const d = JSON.parse(code); const d = JSON.parse(code);
@ -58,7 +156,18 @@ export function codePlugin(): Plugin[] {
} }
} }
code = JSON.stringify(d); // 转字符串不然会报错Method too large
if (id.includes("/locale/")) {
let t: string[] = [];
(d as string[][]).forEach(([a, b]) => {
t.push(`${a}<__=__>${b}`);
});
code = JSON.stringify([[t.join("<__&__>")]]);
} else {
code = JSON.stringify(d);
}
} }
return { return {

View File

@ -27,18 +27,31 @@ export function flatten(template: string): string {
let header = template.substring(0, startIndex); let header = template.substring(0, startIndex);
// 获取 Service 类型定义及其内容,去除换行和制表符 // 获取 Service 类型定义及其内容,去除换行和制表符
const serviceContent = template.substring(startIndex).replace(/\n|\t/g, ""); const serviceTemplateContent = template.substring(startIndex).replace(/\n|\t/g, "");
// 找到 Service 的内容部分
const serviceStartIndex = serviceTemplateContent.indexOf("{") + 1;
const serviceEndIndex = findClosingBrace(serviceTemplateContent, serviceStartIndex);
const serviceInnerContent = serviceTemplateContent
.substring(serviceStartIndex, serviceEndIndex)
.trim();
// 存储所有接口定义
const allInterfaces = new Map<string, string>();
// 处理 Service 内容,保持原有结构但替换嵌套对象为接口引用
const serviceContent = buildCurrentLevelContent(serviceInnerContent);
// 递归收集所有需要生成的接口
flattenContent(serviceInnerContent, allInterfaces, []);
// 生成所有接口定义
let interfaces = ""; let interfaces = "";
let serviceFields = ""; allInterfaces.forEach((content, key) => {
interfaces += `\nexport interface ${firstUpperCase(key)}Interface { ${content} }\n`;
// 解析内容并生成接口定义
parse(serviceContent).forEach(({ key, content, level }) => {
interfaces += `\nexport interface ${firstUpperCase(key)}Interface {${content}}\n`;
serviceFields += `${key}: ${firstUpperCase(key)}Interface;`;
}); });
return `${header}${interfaces}\nexport type Service = {${serviceFields}}`; return `${header}${interfaces}\nexport type Service = { ${serviceContent} }`;
} }
/** /**
@ -66,45 +79,64 @@ function findClosingBrace(str: string, startIndex: number): number {
} }
/** /**
* *
* @param content - * @param content -
* @returns * @param allInterfaces - Map
* @param parentFields - 使
*/ */
function parse(content: string, level: number = 0): ParseResult[] { function flattenContent(
// 匹配形如 xxx: { ... } 的结构 content: string,
allInterfaces: Map<string, string>,
parentFields: string[],
): void {
const interfacePattern = /(\w+)\s*:\s*\{/g; const interfacePattern = /(\w+)\s*:\s*\{/g;
const result: ParseResult[] = [];
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
while ((match = interfacePattern.exec(content)) !== null) { while ((match = interfacePattern.exec(content)) !== null) {
const key = match[1];
const startIndex = match.index + match[0].length; const startIndex = match.index + match[0].length;
const endIndex = findClosingBrace(content, startIndex); const endIndex = findClosingBrace(content, startIndex);
if (endIndex > startIndex) { if (endIndex > startIndex) {
let parsedContent = content.substring(startIndex, endIndex).trim(); const innerContent = content.substring(startIndex, endIndex).trim();
// 处理嵌套结构 // 构建当前接口的内容,将嵌套对象替换为接口引用
if (parsedContent.includes("{") && parsedContent.includes("}")) { const currentLevelContent = buildCurrentLevelContent(innerContent);
const nestedInterfaces = parse(parsedContent, level + 1); allInterfaces.set(key, currentLevelContent);
// 替换嵌套的内容为接口引用 // 递归处理嵌套内容
if (nestedInterfaces.length > 0) { flattenContent(innerContent, allInterfaces, []);
nestedInterfaces.forEach((nestedInterface) => { }
const pattern = `${nestedInterface.key}: {${nestedInterface.content}};`; }
const replacement = `${nestedInterface.key}: ${firstUpperCase(nestedInterface.key)}Interface`; }
parsedContent = parsedContent.replace(pattern, replacement);
});
}
}
// 将解析结果添加到数组开头 /**
result.unshift({ *
key: match[1], * @param content -
level, * @returns
content: parsedContent, */
}); function buildCurrentLevelContent(content: string): string {
const interfacePattern = /(\w+)\s*:\s*\{/g;
let result = content;
let match: RegExpExecArray | null;
// 重置正则表达式的 lastIndex
interfacePattern.lastIndex = 0;
while ((match = interfacePattern.exec(content)) !== null) {
const key = match[1];
const startIndex = match.index + match[0].length;
const endIndex = findClosingBrace(content, startIndex);
if (endIndex > startIndex) {
const fullMatch = content.substring(match.index, endIndex + 1);
const replacement = `${key}: ${firstUpperCase(key)}Interface;`;
result = result.replace(fullMatch, replacement);
} }
} }
// 清理多余的分号和空格
result = result.replace(/;+/g, ";").replace(/\s+/g, " ").trim();
return result; return result;
} }

View File

@ -94,9 +94,6 @@ function postcssPlugin(): Plugin {
{ {
postcssPlugin: "vite-cool-uniappx-class-mapping", postcssPlugin: "vite-cool-uniappx-class-mapping",
prepare() { prepare() {
// 存储 Tailwind 颜色值
const colorValues: Record<string, string> = {};
return { return {
// 处理选择器规则 // 处理选择器规则
Rule(rule: any) { Rule(rule: any) {

View File

@ -40,14 +40,70 @@ export function createDir(path: string, recursive?: boolean) {
export function readFile(path: string, json?: boolean) { export function readFile(path: string, json?: boolean) {
try { try {
const content = fs.readFileSync(path, "utf8"); const content = fs.readFileSync(path, "utf8");
return json return json ? JSON.parse(removeJsonComments(content)) : content;
? JSON.parse(content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
: content;
} catch (err) {} } catch (err) {}
return ""; return "";
} }
// 安全地移除JSON中的注释
function removeJsonComments(content: string): string {
let result = "";
let inString = false;
let stringChar = "";
let escaped = false;
let i = 0;
while (i < content.length) {
const char = content[i];
const nextChar = content[i + 1];
// 处理字符串状态
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
result += char;
} else if (inString && char === stringChar && !escaped) {
inString = false;
stringChar = "";
result += char;
} else if (inString) {
// 在字符串内,直接添加字符
result += char;
escaped = char === "\\" && !escaped;
} else {
// 不在字符串内,检查注释
if (char === "/" && nextChar === "/") {
// 单行注释,跳过到行尾
while (i < content.length && content[i] !== "\n") {
i++;
}
if (i < content.length) {
result += content[i]; // 保留换行符
}
} else if (char === "/" && nextChar === "*") {
// 多行注释,跳过到 */
i += 2;
while (i < content.length - 1) {
if (content[i] === "*" && content[i + 1] === "/") {
i += 2;
break;
}
i++;
}
continue;
} else {
result += char;
escaped = false;
}
}
i++;
}
return result;
}
// 写入文件 // 写入文件
export function writeFile(path: string, data: any) { export function writeFile(path: string, data: any) {
try { try {
@ -93,3 +149,26 @@ export function error(message: string) {
export function success(message: string) { export function success(message: string) {
console.log("\x1B[32m%s\x1B[0m", message); console.log("\x1B[32m%s\x1B[0m", message);
} }
/**
*
* @param version1 1 (: "1.2.3")
* @param version2 2 (: "1.2.4")
* @returns 1: version1 > version2, 0: 相等, -1: version1 < version2
*/
export function compareVersion(version1: string, version2: string): number {
const v1Parts = version1.split(".").map(Number);
const v2Parts = version2.split(".").map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}

View File

@ -119,5 +119,7 @@ export declare namespace Config {
// 暗黑模式文本类名 // 暗黑模式文本类名
darkTextClass?: string; darkTextClass?: string;
}; };
// 是否纯净版
clean?: boolean;
} }
} }

View File

@ -1,642 +1,428 @@
<template> <template>
<div class="cl-upload__wrap" :class="[customClass]"> <el-button :icon="icon" :disabled="disabled" :type="type" @click="open">
<div {{ $t('导入') }}
class="cl-upload" </el-button>
:class="[
`cl-upload--${type}`, <cl-form ref="Form">
{ <template #slot-upload>
'is-disabled': disabled, <div v-if="!upload.filename" class="upload">
'is-multiple': multiple, <div class="tips" v-if="template">
'is-small': small <span>{{ tips }}</span>
} <el-button type="primary" text bg @click="download">{{
]" $t('下载模版')
> }}</el-button>
<template v-if="!drag">
<div v-if="type == 'file'" class="cl-upload__file-btn">
<el-upload
:ref="setRefs('upload')"
:drag="drag"
action=""
:accept="accept"
:show-file-list="false"
:before-upload="onBeforeUpload"
:http-request="httpRequest"
:headers="headers"
:multiple="multiple"
:disabled="disabled"
>
<slot>
<el-button type="success">{{ text }}</el-button>
</slot>
</el-upload>
</div> </div>
</template>
<!-- 列表 --> <div class="inner">
<vue-draggable <cl-upload
v-if="showList" :ref="setRefs('upload')"
v-model="list" drag
class="cl-upload__list" :limit-size="limitSize"
tag="div"
ghost-class="Ghost"
drag-class="Drag"
item-key="uid"
:disabled="!draggable"
@end="update"
>
<!-- 触发器 -->
<template #footer>
<div v-if="(type == 'image' || drag) && isAdd" class="cl-upload__footer">
<el-upload
:ref="setRefs('upload')"
action=""
:drag="drag"
:accept="accept"
:show-file-list="false"
:before-upload="onBeforeUpload"
:http-request="httpRequest"
:headers="headers"
:multiple="multiple"
:disabled="disabled"
>
<slot>
<!-- 拖拽方式 -->
<div v-if="drag" class="cl-upload__demo is-dragger">
<el-icon :size="46">
<upload-filled />
</el-icon>
<div>
{{
t('点击上传或将文件拖动到此处,文件大小限制{n}M', {
n: limitSize
})
}}
</div>
</div>
<!-- 点击方式 -->
<div v-else class="cl-upload__demo">
<el-icon :size="36">
<component :is="icon" v-if="icon" />
<picture-filled v-else />
</el-icon>
<span v-if="text" class="text">{{ text }}</span>
</div>
</slot>
</el-upload>
</div>
</template>
<!-- 列表 -->
<template #item="{ element: item, index }">
<el-upload
action=""
:accept="accept" :accept="accept"
:show-file-list="false"
:http-request="
req => {
return httpRequest(req, item);
}
"
:before-upload="
file => {
onBeforeUpload(file, item);
}
"
:headers="headers"
:disabled="disabled" :disabled="disabled"
> :auto-upload="false"
<slot name="item" :item="item" :index="index"> :before-upload="onUpload"
<div class="cl-upload__item"> :size="[220, '100%']"
<upload-item />
:show-tag="showTag" </div>
:item="item" </div>
:list="list" </template>
:disabled="disabled"
:deletable="deletable"
@remove="remove(index)"
/>
<!-- 小图模式 --> <template #slot-list>
<el-icon <div v-if="list.length" class="data-table">
v-if="small" <div class="head">
class="cl-upload__item-remove" <el-button type="success" @click="clear">{{ $t('重新上传') }}</el-button>
@click.stop="remove(index)" <el-button
type="danger"
:disabled="table.selection.length == 0"
@click="table.del()"
>
{{ $t('批量删除') }}
</el-button>
</div>
<div class="cl-table">
<el-table
border
:data="list"
max-height="600px"
@selection-change="table.onSelectionChange"
@row-click="
row => {
row._edit = true;
}
"
>
<el-table-column
type="selection"
width="60px"
align="center"
fixed="left"
/>
<el-table-column
:label="$t('序号')"
type="index"
width="80px"
align="center"
fixed="left"
:index="table.onIndex"
/>
<el-table-column
v-for="item in table.header"
:key="item"
:prop="item"
:label="item"
min-width="160px"
align="center"
>
<template #default="scope">
<span v-if="!scope.row._edit">{{ scope.row[item] }}</span>
<template v-else>
<el-input
v-model="scope.row[item]"
type="textarea"
clearable
:placeholder="item"
/>
</template>
</template>
</el-table-column>
<el-table-column
:label="$t('操作')"
width="100px"
align="center"
fixed="right"
>
<template #default="scope">
<el-button
text
bg
type="danger"
@click.stop="table.del(scope.$index)"
> >
<circle-close-filled /> {{ $t('删除') }}
</el-icon> </el-button>
</div> </template>
</slot> </el-table-column>
</el-upload> </el-table>
</template> </div>
</vue-draggable>
</div> <div class="pagination">
</div> <el-pagination
v-model:current-page="pagination.page"
background
layout="total, prev, pager, next"
:total="upload.list.length"
:page-size="pagination.size"
@current-change="pagination.onCurrentChange"
/>
</div>
</div>
</template>
</cl-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ defineOptions({
name: 'cl-upload' name: 'cl-import-btn'
}); });
import { computed, ref, watch, type PropType, nextTick } from 'vue';
import { assign, isArray, isEmpty, isNumber } from 'lodash-es';
import VueDraggable from 'vuedraggable';
import { ElMessage } from 'element-plus';
import { PictureFilled, UploadFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import { useForm } from '@cool-vue/crud'; import { useForm } from '@cool-vue/crud';
import { useCool } from '/@/cool'; import { ElMessage } from 'element-plus';
import { useBase } from '/$/base'; import { reactive, type PropType, computed } from 'vue';
import { uuid, isPromise } from '/@/cool/utils'; import * as XLSX from 'xlsx';
import { getUrls, getType } from '../utils'; import chardet from 'chardet';
import { useUpload } from '../hooks'; import { extname } from '/@/cool/utils';
import UploadItem from './upload-item/index.vue'; import { has } from 'lodash-es';
import { CrudProps } from '/#/crud';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useCool } from '/@/cool';
const props = defineProps({ const props = defineProps({
...CrudProps, onConfig: Function,
// onSubmit: Function,
modelValue: { template: {
type: [String, Array], type: String,
default: () => [] default: ''
},
tips: String,
limitSize: {
type: Number,
default: 10
}, },
//
type: { type: {
type: String as PropType<'image' | 'file'>, type: String as PropType<
default: 'image' 'default' | 'success' | 'warning' | 'info' | 'text' | 'primary' | 'danger'
>,
default: 'success'
}, },
// icon: String,
accept: String,
//
multiple: Boolean,
//
limit: Number,
//
limitSize: Number,
//
autoUpload: {
type: Boolean,
default: true
},
//
size: [String, Number, Array],
//
small: Boolean,
//
icon: null,
//
text: String,
//
showTag: {
type: Boolean,
default: true
},
//
showFileList: {
type: Boolean,
default: true
},
//
draggable: Boolean,
//
drag: Boolean,
//
disabled: Boolean, disabled: Boolean,
// accept: {
deletable: Boolean, type: String,
// default:
customClass: String, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel,text/csv'
// }
beforeUpload: Function,
//
prefixPath: String
}); });
const emit = defineEmits(['update:modelValue', 'change', 'upload', 'success', 'error', 'progress']); const emit = defineEmits(['change']);
const { refs, setRefs } = useCool();
const { user } = useBase();
const Form = useForm(); const Form = useForm();
const { options, toUpload } = useUpload();
const { t } = useI18n(); const { t } = useI18n();
const { refs, setRefs } = useCool();
// //
const size = computed(() => { const tips = computed(() => {
const d = props.size || options.size; return props.tips || t('请按照模版填写信息');
return (isArray(d) ? d : [d, d]).map((e: string | number) => (isNumber(e) ? e + 'px' : e));
}); });
// //
const disabled = computed(() => { const upload = reactive({
return props.isDisabled || props.disabled; filename: '',
file: null as File | null,
list: [] as any[]
}); });
// //
const limit = props.limit || options.limit.upload; const pagination = reactive({
size: 20,
// page: 1,
const limitSize = props.limitSize || options.limit.size; onCurrentChange(page: number) {
pagination.page = page;
//
const text = computed(() => {
if (props.text !== undefined) {
return props.text;
} else {
switch (props.type) {
case 'file':
return t('选择文件');
case 'image':
return t('选择图片');
default:
return '';
}
} }
}); });
// //
const headers = computed(() => { const table = reactive({
return { //
Authorization: user.token header: [] as string[],
};
});
// //
const list = ref<Upload.Item[]>([]); selection: [] as any[],
// //
const showList = computed(() => { del(index?: number) {
if (props.type == 'file') { if (index !== undefined) {
return props.showFileList ? !isEmpty(list.value) : false; upload.list.splice(index, 1);
} else {
return true;
}
});
//
const accept = computed(() => {
return props.accept || (props.type == 'file' ? '' : 'image/*');
});
//
const isAdd = computed(() => {
const len = list.value.length;
if (props.multiple && !disabled.value) {
return limit - len > 0;
}
return len == 0;
});
//
async function onBeforeUpload(file: any, item?: Upload.Item) {
function next() {
const d = {
uid: file.uid,
size: file.size,
name: file.name,
type: getType(file.name),
progress: props.autoUpload ? 0 : 100, // 100%
url: '',
preload: '',
error: ''
};
//
if (d.type == 'image') {
if (file instanceof File) {
d.preload = window.webkitURL.createObjectURL(file);
}
}
//
emit('upload', d, file);
//
if (item) {
assign(item, d);
} else { } else {
if (props.multiple) { upload.list = upload.list.filter(a => {
if (!isAdd.value) { return !table.selection.includes(a._index);
ElMessage.warning(t('最多只能上传{n}个文件', { n: limit })); });
return false;
} else {
list.value.push(d);
}
} else {
list.value = [d];
}
} }
},
return true; //
onIndex(index: number) {
return index + 1 + (pagination.page - 1) * pagination.size;
},
//
onSelectionChange(arr: any[]) {
table.selection = arr.map(e => e._index);
} }
});
// //
if (file.size / 1024 / 1024 >= limitSize) { const list = computed(() => {
ElMessage.error(t('上传文件大小不能超过 {n}MB!', { n: limitSize })); return upload.list.filter((_, i) => {
return false; return (
} i >= (pagination.page - 1) * pagination.size && i < pagination.page * pagination.size
);
// });
if (props.beforeUpload) { });
let r = props.beforeUpload(file, item, { next });
if (isPromise(r)) {
r.then(next).catch(() => null);
} else {
if (r) {
r = next();
}
}
return r;
} else {
return next();
}
}
//
function remove(index: number) {
list.value.splice(index, 1);
update();
}
// //
function clear() { function clear() {
list.value = []; upload.filename = '';
upload.file = null;
upload.list = [];
table.header = [];
table.selection = [];
refs.upload?.clear();
} }
// //
async function httpRequest(req: any, item?: Upload.Item) { function open() {
if (!item) {
item = list.value.find(e => e.uid == req.file.uid);
}
if (!item) {
return false;
}
//
toUpload(req.file, {
prefixPath: props.prefixPath,
onProgress(progress) {
item!.progress = progress;
emit('progress', item);
}
})
.then(res => {
assign(item!, res);
emit('success', item);
update();
})
.catch(err => {
item!.error = err.message;
emit('error', item);
});
}
//
function check() {
return list.value.find(e => !e.url);
}
//
function update() {
if (!check()) {
const urls = getUrls(list.value);
const val = props.multiple ? getUrls(list.value) : urls[0] || '';
//
emit('update:modelValue', val);
emit('change', val);
nextTick(() => {
if (props.prop) {
Form.value?.validateField(props.prop);
}
//
refs.upload?.clearFiles();
});
}
}
//
function upload(file: File) {
clear(); clear();
refs.upload?.clearFiles(); Form.value?.open({
title: t('导入'),
width: computed(() => (upload.filename ? '80%' : '800px')),
dialog: {
'close-on-press-escape': false
},
items: [
...(props.onConfig ? props.onConfig(Form) : []),
{
prop: 'file',
component: {
name: 'slot-upload'
},
hidden() {
return upload.filename;
}
},
{
component: {
name: 'slot-list'
}
}
],
op: {
saveButtonText: t('提交')
},
on: {
submit(_, { done, close }) {
if (!upload.filename) {
done();
return ElMessage.error(t('请选择文件'));
}
nextTick(() => { if (props.onSubmit) {
refs.upload?.handleStart(file); props.onSubmit(
refs.upload?.submit(); {
...upload,
..._
},
{ done, close }
);
} else {
ElMessage.error(t('[cl-import-btn] onSubmit is required'));
done();
}
}
}
}); });
} }
// //
watch( function onUpload(raw: File, _: any, { next }: any) {
() => props.modelValue, const reader = new FileReader();
(val: any[] | string) => { const ext = extname(raw.name);
if (check()) {
return false;
}
const urls = (isArray(val) ? val : [val]).filter(Boolean); reader.onload = (event: any) => {
try {
let data = '';
list.value = urls if (ext == 'csv') {
.map((url, index) => { const detected: any = chardet.detect(new Uint8Array(event.target.result));
const old = list.value[index] || {}; const decoder = new TextDecoder(detected);
data = decoder.decode(event.target.result);
} else {
data = event.target.result;
}
return assign( const workbook = XLSX.read(data, { type: 'binary', raw: ext == 'csv' });
{
progress: 100, let json: any[] = [];
uid: uuid() for (const sheet in workbook.Sheets) {
}, if (has(workbook.Sheets, sheet)) {
old, json = json.concat(
{ XLSX.utils.sheet_to_json(workbook.Sheets[sheet], {
type: getType(url), raw: false,
url, dateNF: 'yyyy-mm-dd',
preload: old.url == url ? old.preload : url // defval: ''
} })
); );
}) }
.filter((_, i) => { }
return props.multiple ? true : i == 0;
upload.list = json.map((e, i) => {
return {
...e,
_index: i
};
}); });
}, upload.filename = raw.name;
{ upload.file = raw;
immediate: true
} const sheet = workbook.Sheets[Object.keys(workbook.Sheets)[0]];
);
for (const i in sheet) {
if (i[0] === '!') continue;
const row = i.match(/[0-9]+/)?.[0];
if (row == '1') {
table.header.push(sheet[i].v);
}
}
emit('change', json);
} catch (err) {
ElMessage.error(t('文件异常,请检查内容是否正确'));
clear();
}
};
if (ext == 'csv') {
reader.readAsArrayBuffer(raw);
} else {
reader.readAsBinaryString(raw);
}
next();
return false;
}
//
function download() {
const link = document.createElement('a');
link.setAttribute('href', props.template);
link.setAttribute('download', '');
link.click();
}
//
defineExpose({ defineExpose({
isAdd, open,
list,
check,
clear, clear,
remove, Form
upload
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-upload { .upload {
line-height: normal; display: flex;
flex-direction: column;
.Ghost { .inner {
.cl-upload__item {
border: 1px dashed var(--el-color-primary) !important;
}
}
&__file {
width: 100%; width: 100%;
&-btn { :deep(.cl-upload) {
width: fit-content; .cl-upload__footer,
} .cl-upload__list,
} .el-upload,
.is-drag {
&__list { width: 100% !important;
display: flex;
flex-wrap: wrap;
}
&__item,
&__demo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: v-bind('size[0]');
width: v-bind('size[1]');
background-color: var(--el-fill-color-light);
color: var(--el-text-color-regular);
border-radius: 8px;
cursor: pointer;
box-sizing: border-box;
position: relative;
user-select: none;
}
&__demo {
font-size: 13px;
.el-icon {
font-size: 46px;
}
.text {
margin-top: 5px;
}
&.is-dragger {
padding: 20px;
}
}
&__file-btn {
& + .cl-upload__list {
margin-top: 10px;
}
}
:deep(.el-upload) {
display: block;
.el-upload-dragger {
padding: 0;
border: 0;
background-color: transparent !important;
position: relative;
&.is-dragover {
&::after {
display: block;
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
pointer-events: none;
border-radius: 8px;
box-sizing: border-box;
border: 1px dashed var(--el-color-primary);
}
}
}
}
&.is-disabled {
.cl-upload__demo {
color: var(--el-text-color-placeholder);
}
:deep(.cl-upload__item) {
cursor: not-allowed;
background-color: var(--el-disabled-bg-color);
}
}
&.is-multiple {
.cl-upload__list {
margin-bottom: -5px;
}
.cl-upload__item {
margin: 0 5px 5px 0;
}
.cl-upload__footer {
margin-bottom: 5px;
}
}
&.is-small {
.cl-upload__demo {
.el-icon {
font-size: 20px !important;
}
.text {
display: none;
}
}
.cl-upload__item-remove {
position: absolute;
right: 0px;
top: 0px;
color: var(--el-color-danger);
background-color: #fff;
border-radius: 100%;
}
:deep(.cl-upload-item) {
.cl-upload-item__progress-bar,
.cl-upload-item__actions,
.cl-upload-item__tag {
display: none;
}
.cl-upload-item__progress-value {
font-size: 12px;
}
}
}
&:not(.is-disabled) {
.cl-upload__demo {
&:hover {
color: var(--el-color-primary);
} }
} }
} }
} }
.tips {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
& > span {
color: var(--el-color-warning);
}
}
.data-table {
.head {
margin-bottom: 10px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
}
</style> </style>