| New file |
| | |
| | | # WCS 国际化语言包说明 |
| | | |
| | | ## 读取位置 |
| | | |
| | | 当前实现有两层语言包来源: |
| | | |
| | | 1. 内置语言包 |
| | | |
| | | ```text |
| | | classpath:/i18n/<locale>/ |
| | | ``` |
| | | |
| | | 也就是仓库里的: |
| | | |
| | | ```text |
| | | src/main/resources/i18n/<locale>/ |
| | | ``` |
| | | |
| | | 这一层是项目自带的默认语言包,保证系统开箱可用。 |
| | | |
| | | 2. 外置语言包 |
| | | |
| | | ```text |
| | | ./stock/out/wcs/i18n/<locale>/ |
| | | ``` |
| | | |
| | | 这一层是运行时可安装、可替换、可覆盖的语言包目录,对应 `application.yml` 里的 `app.i18n.pack-path`。 |
| | | |
| | | 实际加载顺序: |
| | | |
| | | 1. 先读内置语言包 |
| | | 2. 再读外置语言包 |
| | | 3. 外置语言包同名 key 会覆盖内置值 |
| | | |
| | | 当前已配置语言: |
| | | |
| | | - `zh-CN` |
| | | - `en-US` |
| | | |
| | | 外置目录示例: |
| | | |
| | | ```text |
| | | stock/out/wcs/i18n/ |
| | | en-US/ |
| | | messages.properties |
| | | legacy.properties |
| | | zh-CN/ |
| | | messages.properties |
| | | legacy.properties |
| | | ``` |
| | | |
| | | ## 文件用途 |
| | | |
| | | ### `messages.properties` |
| | | |
| | | 用于稳定的 key 形式国际化。 |
| | | |
| | | 适合以下内容: |
| | | |
| | | - 菜单名称 |
| | | - 页面标题 |
| | | - 对话框标题 |
| | | - 按钮文案 |
| | | - 状态名称 |
| | | - 新增功能文案 |
| | | |
| | | ### `legacy.properties` |
| | | |
| | | 用于旧页面的纯文本兼容替换。 |
| | | |
| | | 这个项目里老的 Layui/jQuery 页面很多,存在大量直接写死的中文;这层是过渡方案,便于先让英文可用,再逐步把旧页面改成 key 化。 |
| | | |
| | | ## key 规则 |
| | | |
| | | ### 菜单 / 资源 key |
| | | |
| | | 后端菜单翻译 key 由 `sys_resource.code` 推导。 |
| | | |
| | | 例如: |
| | | |
| | | ```properties |
| | | resource.develop=Development |
| | | resource.ai.llm_config=AI Configuration |
| | | resource.notifyReport.notifyReport=Notification Report |
| | | resource.ai.llm_config.view=View |
| | | ``` |
| | | |
| | | ### 权限 key |
| | | |
| | | 权限翻译 key 由 `action` 推导。 |
| | | |
| | | 例如: |
| | | |
| | | ```properties |
| | | permission.function=Specified Functions |
| | | permission.user.resetPassword=Reset Password |
| | | ``` |
| | | |
| | | ### 通用 UI key |
| | | |
| | | 例如: |
| | | |
| | | ```properties |
| | | common.profile=Profile |
| | | common.logout=Log Out |
| | | index.homeTab=Control Center |
| | | login.title=WCS System V3.0 |
| | | ``` |
| | | |
| | | ## 安装语言包 |
| | | |
| | | 如果只是使用项目自带中文和英文,不需要额外操作。 |
| | | |
| | | 如果要安装外置语言包,按下面做: |
| | | |
| | | 1. 在 `stock/out/wcs/i18n/` 下创建新语言目录。 |
| | | 2. 从现有语言包复制 `messages.properties` 和 `legacy.properties` 作为模板。 |
| | | 3. 只翻译 value,不要改 key。 |
| | | 4. 刷新页面,或重启服务。 |
| | | |
| | | 系统会按配置周期检查外置目录,因此简单文本调整理论上不必重启;但正式环境建议仍按发布流程重启或重载。 |
| | | |
| | | ## 推荐维护方式 |
| | | |
| | | 1. 新增页面和新增接口文案,优先写入 `messages.properties`。 |
| | | 2. 老页面先通过 `legacy.properties` 兼容,不要一开始就全量重构。 |
| | | 3. 某个老页面重构时,再把它的纯文本逐步迁移到显式 key。 |
| | | |
| | | ## 说明 |
| | | |
| | | - 默认语言是 `zh-CN` |
| | | - 前端请求会自动携带 `X-Lang` |
| | | - 登录页和首页都已经支持语言切换 |
| | | - 非默认语言如果缺少菜单 key,后端会先尝试根据 `resource.code` 自动生成可读名称,再回退到原始中文名 |
| | |
| | | response.setHeader("Access-Control-Allow-Origin", "*"); |
| | | response.setHeader("Access-Control-Allow-Credentials", "true"); |
| | | response.setHeader("Access-Control-Allow-Methods", "*"); |
| | | response.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token"); |
| | | response.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token,token,X-Lang,Accept-Language"); |
| | | response.setHeader("Access-Control-Expose-Headers", "*"); |
| | | |
| | | } |
| | |
| | | |
| | | import com.core.common.R; |
| | | import com.core.exception.CoolException; |
| | | import com.zy.common.i18n.I18nMessageService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.HttpRequestMethodNotSupportedException; |
| | | import org.springframework.web.bind.annotation.ExceptionHandler; |
| | | import org.springframework.web.bind.annotation.RestControllerAdvice; |
| | |
| | | @RestControllerAdvice |
| | | public class CoolExceptionHandler { |
| | | |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @ExceptionHandler(Exception.class) |
| | | public R handlerException(HandlerMethod handler, Exception e) { |
| | | e.printStackTrace(); |
| | | return R.error(); |
| | | return R.error(i18nMessageService.getMessage("response.common.systemError")); |
| | | } |
| | | |
| | | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) |
| | | public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { |
| | | return R.error(); |
| | | return R.error(i18nMessageService.getMessage("response.common.methodNotAllowed")); |
| | | } |
| | | |
| | | @ExceptionHandler(CoolException.class) |
| | |
| | | package com.zy.common.config; |
| | | |
| | | import com.zy.common.i18n.RequestLocaleInterceptor; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
| | |
| | | public class WebConfig implements WebMvcConfigurer { |
| | | |
| | | @Autowired |
| | | private RequestLocaleInterceptor requestLocaleInterceptor; |
| | | |
| | | @Autowired |
| | | private AdminInterceptor adminInterceptor; |
| | | |
| | | @Override |
| | | public void addInterceptors(InterceptorRegistry registry) { |
| | | registry.addInterceptor(requestLocaleInterceptor) |
| | | .addPathPatterns("/**"); |
| | | registry.addInterceptor(adminInterceptor) |
| | | .addPathPatterns("/**") |
| | | ; |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import com.core.common.R; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.*; |
| | | |
| | | @RestController |
| | | @RequestMapping("/i18n") |
| | | public class I18nController { |
| | | |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @RequestMapping("/messages") |
| | | public R messages(@RequestParam(required = false) String lang) { |
| | | Locale locale = i18nMessageService.resolveLocale(lang); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("locale", I18nLocaleUtils.toTag(locale)); |
| | | result.put("defaultLocale", i18nMessageService.getDefaultLocaleTag()); |
| | | result.put("supportedLocales", localeOptions(locale)); |
| | | result.put("messages", i18nMessageService.getMessages(locale)); |
| | | result.put("legacy", i18nMessageService.getLegacyMessages(locale)); |
| | | return R.ok(result); |
| | | } |
| | | |
| | | private List<Map<String, String>> localeOptions(Locale locale) { |
| | | List<Map<String, String>> options = new ArrayList<>(); |
| | | for (String supportedLocale : i18nMessageService.getSupportedLocaleTags()) { |
| | | LinkedHashMap<String, String> option = new LinkedHashMap<>(); |
| | | option.put("tag", supportedLocale); |
| | | option.put("label", i18nMessageService.getMessage("lang." + supportedLocale, locale)); |
| | | options.add(option); |
| | | } |
| | | return options; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import com.core.common.Cools; |
| | | |
| | | import java.util.*; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | public class I18nLocaleUtils { |
| | | |
| | | private static final Set<String> UPPERCASE_WORDS = new HashSet<>( |
| | | Arrays.asList("ai", "api", "wcs", "wms", "llm", "rgv", "crn", "plc", "http", "mcp", "erp", "io") |
| | | ); |
| | | |
| | | private static final Pattern WORD_BOUNDARY = Pattern.compile("([a-z0-9])([A-Z])"); |
| | | |
| | | private I18nLocaleUtils() { |
| | | } |
| | | |
| | | public static Locale defaultLocale(I18nProperties properties) { |
| | | return toLocale(properties.getDefaultLocale()); |
| | | } |
| | | |
| | | public static Locale toLocale(String value) { |
| | | if (Cools.isEmpty(value)) { |
| | | return Locale.SIMPLIFIED_CHINESE; |
| | | } |
| | | String normalized = value.replace('_', '-').trim(); |
| | | return Locale.forLanguageTag(normalized); |
| | | } |
| | | |
| | | public static String toTag(Locale locale) { |
| | | if (locale == null) { |
| | | return "zh-CN"; |
| | | } |
| | | String tag = locale.toLanguageTag(); |
| | | return Cools.isEmpty(tag) || "und".equalsIgnoreCase(tag) ? "zh-CN" : tag; |
| | | } |
| | | |
| | | public static Locale resolveLocale(String requested, I18nProperties properties) { |
| | | Locale fallback = defaultLocale(properties); |
| | | if (Cools.isEmpty(requested)) { |
| | | return fallback; |
| | | } |
| | | String requestedTag = requested.replace('_', '-').trim(); |
| | | if (requestedTag.contains(",")) { |
| | | requestedTag = requestedTag.substring(0, requestedTag.indexOf(',')).trim(); |
| | | } |
| | | if (requestedTag.contains(";")) { |
| | | requestedTag = requestedTag.substring(0, requestedTag.indexOf(';')).trim(); |
| | | } |
| | | for (String supported : properties.getSupportedLocales()) { |
| | | String supportedTag = supported.replace('_', '-').trim(); |
| | | if (supportedTag.equalsIgnoreCase(requestedTag)) { |
| | | return toLocale(supportedTag); |
| | | } |
| | | Locale supportedLocale = toLocale(supportedTag); |
| | | if (supportedLocale.getLanguage().equalsIgnoreCase(toLocale(requestedTag).getLanguage())) { |
| | | return supportedLocale; |
| | | } |
| | | } |
| | | return fallback; |
| | | } |
| | | |
| | | public static boolean isDefaultLocale(Locale locale, I18nProperties properties) { |
| | | return toTag(defaultLocale(properties)).equalsIgnoreCase(toTag(locale)); |
| | | } |
| | | |
| | | public static String humanizeCode(String code) { |
| | | if (Cools.isEmpty(code)) { |
| | | return null; |
| | | } |
| | | String normalized = code; |
| | | int hashIndex = normalized.indexOf('#'); |
| | | if (hashIndex > -1 && hashIndex < normalized.length() - 1) { |
| | | normalized = normalized.substring(hashIndex + 1); |
| | | } else { |
| | | int slashIndex = normalized.lastIndexOf('/'); |
| | | if (slashIndex > -1 && slashIndex < normalized.length() - 1) { |
| | | normalized = normalized.substring(slashIndex + 1); |
| | | } |
| | | if (normalized.endsWith(".html")) { |
| | | normalized = normalized.substring(0, normalized.length() - 5); |
| | | } |
| | | } |
| | | normalized = normalized.replaceAll("[^A-Za-z0-9_\\-]+", " "); |
| | | Matcher matcher = WORD_BOUNDARY.matcher(normalized); |
| | | normalized = matcher.replaceAll("$1 $2"); |
| | | normalized = normalized.replace('_', ' ').replace('-', ' '); |
| | | String[] parts = normalized.trim().split("\\s+"); |
| | | if (parts.length == 0) { |
| | | return code; |
| | | } |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (String part : parts) { |
| | | if (Cools.isEmpty(part)) { |
| | | continue; |
| | | } |
| | | if (builder.length() > 0) { |
| | | builder.append(' '); |
| | | } |
| | | String lower = part.toLowerCase(Locale.ENGLISH); |
| | | if (UPPERCASE_WORDS.contains(lower)) { |
| | | builder.append(lower.toUpperCase(Locale.ENGLISH)); |
| | | } else { |
| | | builder.append(Character.toUpperCase(part.charAt(0))); |
| | | if (part.length() > 1) { |
| | | builder.append(part.substring(1)); |
| | | } |
| | | } |
| | | } |
| | | return builder.length() == 0 ? code : builder.toString(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import com.core.common.Cools; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.core.io.ClassPathResource; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.context.i18n.LocaleContextHolder; |
| | | |
| | | import java.io.File; |
| | | import java.io.BufferedReader; |
| | | import java.io.FileInputStream; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.InputStreamReader; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.text.MessageFormat; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | import java.util.regex.PatternSyntaxException; |
| | | |
| | | @Service |
| | | public class I18nMessageService { |
| | | |
| | | private static final String MESSAGE_BUNDLE = "messages"; |
| | | private static final String LEGACY_BUNDLE = "legacy"; |
| | | |
| | | private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>(); |
| | | |
| | | @Autowired |
| | | private I18nProperties properties; |
| | | |
| | | public Locale getCurrentLocale() { |
| | | Locale locale = LocaleContextHolder.getLocale(); |
| | | return locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale; |
| | | } |
| | | |
| | | public Locale resolveLocale(String requested) { |
| | | return I18nLocaleUtils.resolveLocale(requested, properties); |
| | | } |
| | | |
| | | public String getDefaultLocaleTag() { |
| | | return properties.getDefaultLocale(); |
| | | } |
| | | |
| | | public List<String> getSupportedLocaleTags() { |
| | | return properties.getSupportedLocales(); |
| | | } |
| | | |
| | | public String getMessage(String key, Object... args) { |
| | | return getMessage(key, getCurrentLocale(), args); |
| | | } |
| | | |
| | | public String getMessage(String key, Locale locale, Object... args) { |
| | | if (Cools.isEmpty(key)) { |
| | | return ""; |
| | | } |
| | | Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale; |
| | | String value = mergedBundle(resolvedLocale, MESSAGE_BUNDLE).get(key); |
| | | if (value == null && !I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) { |
| | | value = mergedBundle(I18nLocaleUtils.defaultLocale(properties), MESSAGE_BUNDLE).get(key); |
| | | } |
| | | if (value == null) { |
| | | return key; |
| | | } |
| | | return args == null || args.length == 0 ? value : MessageFormat.format(value, args); |
| | | } |
| | | |
| | | public boolean hasMessage(String key, Locale locale) { |
| | | return mergedBundle(locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale, MESSAGE_BUNDLE).containsKey(key); |
| | | } |
| | | |
| | | public Map<String, String> getMessages(Locale locale) { |
| | | return new LinkedHashMap<>(mergedBundle(locale, MESSAGE_BUNDLE)); |
| | | } |
| | | |
| | | public Map<String, String> getLegacyMessages(Locale locale) { |
| | | return new LinkedHashMap<>(mergedBundle(locale, LEGACY_BUNDLE)); |
| | | } |
| | | |
| | | public String translateLegacy(String text) { |
| | | return translateLegacy(text, getCurrentLocale()); |
| | | } |
| | | |
| | | public String translateLegacy(String text, Locale locale) { |
| | | if (Cools.isEmpty(text)) { |
| | | return text; |
| | | } |
| | | Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale; |
| | | if (I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) { |
| | | return text; |
| | | } |
| | | Map<String, String> bundle = mergedBundle(resolvedLocale, LEGACY_BUNDLE); |
| | | String direct = directTranslate(text, bundle); |
| | | if (!text.equals(direct)) { |
| | | return direct; |
| | | } |
| | | String regex = regexTranslate(text, bundle); |
| | | if (!text.equals(regex)) { |
| | | return regex; |
| | | } |
| | | return fragmentTranslate(text, bundle); |
| | | } |
| | | |
| | | public String resolveResourceText(String fallbackName, String code, Long id) { |
| | | Locale locale = getCurrentLocale(); |
| | | String key = resourceKey(code, id); |
| | | if (hasMessage(key, locale)) { |
| | | return getMessage(key, locale); |
| | | } |
| | | if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) { |
| | | String humanized = I18nLocaleUtils.humanizeCode(code); |
| | | if (!Cools.isEmpty(humanized)) { |
| | | return humanized; |
| | | } |
| | | } |
| | | return fallbackName; |
| | | } |
| | | |
| | | public String resolvePermissionText(String fallbackName, String action, Long id) { |
| | | Locale locale = getCurrentLocale(); |
| | | String key = permissionKey(action, id); |
| | | if (hasMessage(key, locale)) { |
| | | return getMessage(key, locale); |
| | | } |
| | | if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) { |
| | | String humanized = I18nLocaleUtils.humanizeCode(action); |
| | | if (!Cools.isEmpty(humanized)) { |
| | | return humanized; |
| | | } |
| | | } |
| | | return fallbackName; |
| | | } |
| | | |
| | | public String resourceKey(String code, Long id) { |
| | | return "resource." + normalizeKey(code, id, "resource"); |
| | | } |
| | | |
| | | public String permissionKey(String action, Long id) { |
| | | return "permission." + normalizeKey(action, id, "permission"); |
| | | } |
| | | |
| | | private Map<String, String> mergedBundle(Locale locale, String bundleName) { |
| | | Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale; |
| | | LinkedHashMap<String, String> merged = new LinkedHashMap<>(); |
| | | Locale defaultLocale = I18nLocaleUtils.defaultLocale(properties); |
| | | merged.putAll(loadBundle(defaultLocale, bundleName)); |
| | | if (!I18nLocaleUtils.toTag(defaultLocale).equalsIgnoreCase(I18nLocaleUtils.toTag(resolvedLocale))) { |
| | | merged.putAll(loadBundle(resolvedLocale, bundleName)); |
| | | } |
| | | return merged; |
| | | } |
| | | |
| | | private Map<String, String> loadBundle(Locale locale, String bundleName) { |
| | | String localeTag = I18nLocaleUtils.toTag(locale); |
| | | String cacheKey = localeTag + ":" + bundleName; |
| | | CacheEntry cacheEntry = cache.get(cacheKey); |
| | | File externalFile = externalBundle(localeTag, bundleName); |
| | | long refreshMillis = Math.max(1, properties.getRefreshSeconds()) * 1000L; |
| | | long externalLastModified = externalFile.exists() ? externalFile.lastModified() : -1L; |
| | | if (cacheEntry != null |
| | | && (System.currentTimeMillis() - cacheEntry.loadedAt) < refreshMillis |
| | | && cacheEntry.externalLastModified == externalLastModified) { |
| | | return cacheEntry.values; |
| | | } |
| | | synchronized (cache.computeIfAbsent(cacheKey, key -> new CacheEntry())) { |
| | | CacheEntry latest = cache.get(cacheKey); |
| | | if (latest != null |
| | | && (System.currentTimeMillis() - latest.loadedAt) < refreshMillis |
| | | && latest.externalLastModified == externalLastModified) { |
| | | return latest.values; |
| | | } |
| | | LinkedHashMap<String, String> values = new LinkedHashMap<>(); |
| | | readClasspathBundle(values, localeTag, bundleName); |
| | | readExternalBundle(values, externalFile); |
| | | CacheEntry updated = new CacheEntry(); |
| | | updated.values = values; |
| | | updated.loadedAt = System.currentTimeMillis(); |
| | | updated.externalLastModified = externalLastModified; |
| | | cache.put(cacheKey, updated); |
| | | return updated.values; |
| | | } |
| | | } |
| | | |
| | | private void readClasspathBundle(Map<String, String> values, String localeTag, String bundleName) { |
| | | ClassPathResource resource = new ClassPathResource("i18n/" + localeTag + "/" + bundleName + ".properties"); |
| | | if (!resource.exists()) { |
| | | return; |
| | | } |
| | | try (InputStream inputStream = resource.getInputStream()) { |
| | | loadProperties(values, inputStream); |
| | | } catch (IOException ex) { |
| | | throw new IllegalStateException("Failed to load classpath i18n bundle: " + resource.getPath(), ex); |
| | | } |
| | | } |
| | | |
| | | private void readExternalBundle(Map<String, String> values, File file) { |
| | | if (!file.exists() || !file.isFile()) { |
| | | return; |
| | | } |
| | | try (InputStream inputStream = new FileInputStream(file)) { |
| | | loadProperties(values, inputStream); |
| | | } catch (IOException ex) { |
| | | throw new IllegalStateException("Failed to load external i18n bundle: " + file.getAbsolutePath(), ex); |
| | | } |
| | | } |
| | | |
| | | private void loadProperties(Map<String, String> values, InputStream inputStream) throws IOException { |
| | | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { |
| | | String line; |
| | | while ((line = reader.readLine()) != null) { |
| | | parsePropertyLine(values, line); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void parsePropertyLine(Map<String, String> values, String line) { |
| | | if (line == null) { |
| | | return; |
| | | } |
| | | String trimmed = line.trim(); |
| | | if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("!")) { |
| | | return; |
| | | } |
| | | int separatorIndex = findSeparator(line); |
| | | String rawKey = separatorIndex >= 0 ? line.substring(0, separatorIndex) : line; |
| | | String rawValue = separatorIndex >= 0 ? line.substring(separatorIndex + 1) : ""; |
| | | while (!rawValue.isEmpty() && Character.isWhitespace(rawValue.charAt(0))) { |
| | | rawValue = rawValue.substring(1); |
| | | } |
| | | String key = unescapePropertyToken(rawKey); |
| | | String value = unescapePropertyToken(rawValue); |
| | | if (!key.isEmpty()) { |
| | | values.put(key, value); |
| | | } |
| | | } |
| | | |
| | | private int findSeparator(String line) { |
| | | boolean escaping = false; |
| | | for (int i = 0; i < line.length(); i++) { |
| | | char ch = line.charAt(i); |
| | | if (escaping) { |
| | | escaping = false; |
| | | continue; |
| | | } |
| | | if (ch == '\\') { |
| | | escaping = true; |
| | | continue; |
| | | } |
| | | if (ch == '=' || ch == ':') { |
| | | return i; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | |
| | | private String unescapePropertyToken(String text) { |
| | | StringBuilder builder = new StringBuilder(text.length()); |
| | | boolean escaping = false; |
| | | for (int i = 0; i < text.length(); i++) { |
| | | char ch = text.charAt(i); |
| | | if (!escaping) { |
| | | if (ch == '\\') { |
| | | escaping = true; |
| | | } else { |
| | | builder.append(ch); |
| | | } |
| | | continue; |
| | | } |
| | | switch (ch) { |
| | | case 't': |
| | | builder.append('\t'); |
| | | break; |
| | | case 'r': |
| | | builder.append('\r'); |
| | | break; |
| | | case 'n': |
| | | builder.append('\n'); |
| | | break; |
| | | case 'f': |
| | | builder.append('\f'); |
| | | break; |
| | | case 'u': |
| | | if (i + 4 < text.length()) { |
| | | String hex = text.substring(i + 1, i + 5); |
| | | try { |
| | | builder.append((char) Integer.parseInt(hex, 16)); |
| | | i += 4; |
| | | break; |
| | | } catch (NumberFormatException ex) { |
| | | builder.append('u'); |
| | | break; |
| | | } |
| | | } |
| | | builder.append('u'); |
| | | break; |
| | | default: |
| | | builder.append(ch); |
| | | break; |
| | | } |
| | | escaping = false; |
| | | } |
| | | if (escaping) { |
| | | builder.append('\\'); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private File externalBundle(String localeTag, String bundleName) { |
| | | return new File(properties.getPackPath(), localeTag + File.separator + bundleName + ".properties"); |
| | | } |
| | | |
| | | private String directTranslate(String text, Map<String, String> bundle) { |
| | | String trimmed = text.trim(); |
| | | String translated = bundle.get(trimmed); |
| | | if (translated == null) { |
| | | return text; |
| | | } |
| | | return preserveOuterWhitespace(text, translated); |
| | | } |
| | | |
| | | private String regexTranslate(String text, Map<String, String> bundle) { |
| | | String trimmed = text == null ? "" : text.trim(); |
| | | for (Map.Entry<String, String> entry : bundle.entrySet()) { |
| | | String key = entry.getKey(); |
| | | if (Cools.isEmpty(key) || !key.startsWith("regex:")) { |
| | | continue; |
| | | } |
| | | String patternText = key.substring("regex:".length()); |
| | | if (Cools.isEmpty(patternText)) { |
| | | continue; |
| | | } |
| | | try { |
| | | Pattern pattern = Pattern.compile(patternText); |
| | | if (!trimmed.isEmpty()) { |
| | | Matcher trimmedMatcher = pattern.matcher(trimmed); |
| | | if (trimmedMatcher.matches()) { |
| | | return preserveOuterWhitespace(text, trimmedMatcher.replaceAll(entry.getValue())); |
| | | } |
| | | } |
| | | Matcher matcher = pattern.matcher(text); |
| | | if (matcher.find()) { |
| | | return matcher.replaceAll(entry.getValue()); |
| | | } |
| | | } catch (PatternSyntaxException ex) { |
| | | // Ignore invalid regex entries so a bad pack does not break all translations. |
| | | } |
| | | } |
| | | return text; |
| | | } |
| | | |
| | | private String fragmentTranslate(String text, Map<String, String> bundle) { |
| | | List<Map.Entry<String, String>> entries = new ArrayList<>(bundle.entrySet()); |
| | | entries.sort((left, right) -> Integer.compare(right.getKey().length(), left.getKey().length())); |
| | | String result = text; |
| | | for (Map.Entry<String, String> entry : entries) { |
| | | if (Cools.isEmpty(entry.getKey()) |
| | | || entry.getKey().length() < 2 |
| | | || entry.getKey().startsWith("regex:") |
| | | || entry.getKey().equals(entry.getValue())) { |
| | | continue; |
| | | } |
| | | result = result.replace(entry.getKey(), entry.getValue()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private String preserveOuterWhitespace(String original, String translated) { |
| | | String trimmed = original == null ? "" : original.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | return translated; |
| | | } |
| | | int leading = original.indexOf(trimmed); |
| | | int trailing = original.length() - leading - trimmed.length(); |
| | | StringBuilder builder = new StringBuilder(); |
| | | if (leading > 0) { |
| | | builder.append(original, 0, leading); |
| | | } |
| | | builder.append(translated); |
| | | if (trailing > 0) { |
| | | builder.append(original.substring(original.length() - trailing)); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String normalizeKey(String raw, Long id, String prefix) { |
| | | if (Cools.isEmpty(raw)) { |
| | | return prefix + "." + (id == null ? "unknown" : id); |
| | | } |
| | | String normalized = raw.replaceAll("\\.html", ""); |
| | | normalized = normalized.replaceAll("[^A-Za-z0-9]+", "."); |
| | | normalized = normalized.replaceAll("\\.+", "."); |
| | | normalized = normalized.replaceAll("^\\.|\\.$", ""); |
| | | if (Cools.isEmpty(normalized)) { |
| | | return prefix + "." + (id == null ? "unknown" : id); |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private static class CacheEntry { |
| | | private Map<String, String> values = new LinkedHashMap<>(); |
| | | private long loadedAt; |
| | | private long externalLastModified = -1L; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | @Component |
| | | @ConfigurationProperties(prefix = "app.i18n") |
| | | public class I18nProperties { |
| | | |
| | | private String defaultLocale = "zh-CN"; |
| | | |
| | | private List<String> supportedLocales = Arrays.asList("zh-CN", "en-US"); |
| | | |
| | | private String packPath = "./stock/out/wcs/i18n"; |
| | | |
| | | private long refreshSeconds = 10; |
| | | |
| | | public String getDefaultLocale() { |
| | | return defaultLocale; |
| | | } |
| | | |
| | | public void setDefaultLocale(String defaultLocale) { |
| | | this.defaultLocale = defaultLocale; |
| | | } |
| | | |
| | | public List<String> getSupportedLocales() { |
| | | return supportedLocales; |
| | | } |
| | | |
| | | public void setSupportedLocales(List<String> supportedLocales) { |
| | | this.supportedLocales = supportedLocales; |
| | | } |
| | | |
| | | public String getPackPath() { |
| | | return packPath; |
| | | } |
| | | |
| | | public void setPackPath(String packPath) { |
| | | this.packPath = packPath; |
| | | } |
| | | |
| | | public long getRefreshSeconds() { |
| | | return refreshSeconds; |
| | | } |
| | | |
| | | public void setRefreshSeconds(long refreshSeconds) { |
| | | this.refreshSeconds = refreshSeconds; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import com.core.common.R; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.core.MethodParameter; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.http.converter.HttpMessageConverter; |
| | | import org.springframework.http.server.ServerHttpRequest; |
| | | import org.springframework.http.server.ServerHttpResponse; |
| | | import org.springframework.web.bind.annotation.RestControllerAdvice; |
| | | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; |
| | | |
| | | @RestControllerAdvice |
| | | public class I18nResponseBodyAdvice implements ResponseBodyAdvice<Object> { |
| | | |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @Override |
| | | public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, |
| | | Class<? extends HttpMessageConverter<?>> selectedConverterType, |
| | | ServerHttpRequest request, ServerHttpResponse response) { |
| | | if (body instanceof R) { |
| | | R result = (R) body; |
| | | Object msg = result.get("msg"); |
| | | if (msg instanceof String) { |
| | | result.put("msg", i18nMessageService.translateLegacy((String) msg)); |
| | | } |
| | | } |
| | | return body; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.common.i18n; |
| | | |
| | | import com.core.common.Cools; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; |
| | | |
| | | import javax.servlet.http.Cookie; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.util.Locale; |
| | | |
| | | @Component |
| | | public class RequestLocaleInterceptor extends HandlerInterceptorAdapter { |
| | | |
| | | public static final String LANG_COOKIE_NAME = "wcs_lang"; |
| | | |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @Override |
| | | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { |
| | | Locale locale = i18nMessageService.resolveLocale(extractLocale(request)); |
| | | org.springframework.context.i18n.LocaleContextHolder.setLocale(locale); |
| | | request.setAttribute("wcsLocale", locale); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { |
| | | org.springframework.context.i18n.LocaleContextHolder.resetLocaleContext(); |
| | | } |
| | | |
| | | private String extractLocale(HttpServletRequest request) { |
| | | String locale = request.getParameter("lang"); |
| | | if (!Cools.isEmpty(locale)) { |
| | | return locale; |
| | | } |
| | | locale = request.getHeader("X-Lang"); |
| | | if (!Cools.isEmpty(locale)) { |
| | | return locale; |
| | | } |
| | | Cookie[] cookies = request.getCookies(); |
| | | if (cookies != null) { |
| | | for (Cookie cookie : cookies) { |
| | | if (LANG_COOKIE_NAME.equals(cookie.getName()) && !Cools.isEmpty(cookie.getValue())) { |
| | | return cookie.getValue(); |
| | | } |
| | | } |
| | | } |
| | | return request.getHeader("Accept-Language"); |
| | | } |
| | | } |
| | |
| | | import com.core.common.R; |
| | | import com.core.exception.CoolException; |
| | | import com.zy.common.CodeRes; |
| | | import com.zy.common.i18n.I18nMessageService; |
| | | import com.zy.common.entity.Parameter; |
| | | import com.zy.common.model.PowerDto; |
| | | import com.zy.common.model.enums.HtmlNavIconType; |
| | |
| | | private RolePermissionService rolePermissionService; |
| | | @Autowired |
| | | private LicenseTimer licenseTimer; |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @RequestMapping("/login.action") |
| | | @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "登录") |
| | | public R loginAction(String mobile, String password){ |
| | | //验证许可证是否有效 |
| | | if (!licenseTimer.getSystemSupport()){ |
| | | return R.parse(CodeRes.SYSTEM_20001); |
| | | return new R(20001, i18nMessageService.getMessage("response.system.licenseExpired")); |
| | | } |
| | | if (mobile.equals("super") && password.equals(Cools.md5(superPwd))) { |
| | | Map<String, Object> res = new HashMap<>(); |
| | |
| | | userWrapper.eq("mobile", mobile); |
| | | User user = userService.selectOne(userWrapper); |
| | | if (Cools.isEmpty(user)){ |
| | | return R.parse(CodeRes.USER_10001); |
| | | return new R(10001, i18nMessageService.getMessage("response.user.notFound")); |
| | | } |
| | | if (user.getStatus()!=1){ |
| | | return R.parse(CodeRes.USER_10002); |
| | | return new R(10002, i18nMessageService.getMessage("response.user.disabled")); |
| | | } |
| | | if (!user.getPassword().equals(password)){ |
| | | return R.parse(CodeRes.USER_10003); |
| | | return new R(10003, i18nMessageService.getMessage("response.user.passwordMismatch")); |
| | | } |
| | | String token = Cools.enToken(System.currentTimeMillis() + mobile, user.getPassword()); |
| | | userLoginService.delete(new EntityWrapper<UserLogin>().eq("user_id", user.getId()).eq("system_type", "WCS")); |
| | |
| | | } |
| | | } |
| | | |
| | | resource.setName(localizeResourceName(resource)); |
| | | subMenu.add(resource); |
| | | iterator.remove(); |
| | | } |
| | |
| | | map.put("menuId", menu.getId()); |
| | | map.put("menuCode", menu.getCode()); |
| | | map.put("menuIcon", HtmlNavIconType.get(menu.getCode())); |
| | | map.put("menu", menu.getName()); |
| | | map.put("menu", localizeResourceName(menu)); |
| | | map.put("subMenu", subMenu); |
| | | result.add(map); |
| | | } |
| | |
| | | for (Resource oneLevel : oneLevels){ |
| | | List<Map> twoLevelsList = new ArrayList<>(); |
| | | Map<String, Object> oneLevelMap = new HashMap<>(); |
| | | oneLevelMap.put("title", oneLevel.getName()); |
| | | oneLevelMap.put("title", localizeResourceName(oneLevel)); |
| | | oneLevelMap.put("id", oneLevel.getId()); |
| | | oneLevelMap.put("spread", true); |
| | | oneLevelMap.put("children", twoLevelsList); |
| | |
| | | // 二级 |
| | | for (Resource twoLevel : twoLevels){ |
| | | Map<String, Object> twoLevelMap = new HashMap<>(); |
| | | twoLevelMap.put("title", twoLevel.getName()); |
| | | twoLevelMap.put("title", localizeResourceName(twoLevel)); |
| | | twoLevelMap.put("id", twoLevel.getId()); |
| | | twoLevelMap.put("spread", false); |
| | | |
| | |
| | | List<Resource> threeLevels = resourceService.selectList(new EntityWrapper<Resource>().eq("resource_id", twoLevel.getId()).eq("level", 3).eq("status", 1).orderBy("sort")); |
| | | for (Resource threeLevel : threeLevels){ |
| | | Map<String, Object> threeLevelMap = new HashMap<>(); |
| | | threeLevelMap.put("title", threeLevel.getName()); |
| | | threeLevelMap.put("title", localizeResourceName(threeLevel)); |
| | | threeLevelMap.put("id", threeLevel.getId()); |
| | | threeLevelMap.put("checked", false); |
| | | threeLevelsList.add(threeLevelMap); |
| | |
| | | |
| | | // 功能模块 |
| | | Map<String, Object> functions = new HashMap<>(); |
| | | functions.put("title", "指定功能"); |
| | | functions.put("title", i18nMessageService.getMessage("permission.function")); |
| | | functions.put("id", "function"); |
| | | functions.put("spread", true); |
| | | List<Map> funcs = new ArrayList<>(); |
| | |
| | | List<Permission> permissions = permissionService.selectList(new EntityWrapper<Permission>().eq("status", 1)); |
| | | for (Permission permission : permissions) { |
| | | Map<String, Object> func = new HashMap<>(); |
| | | func.put("title", permission.getName()); |
| | | func.put("title", i18nMessageService.resolvePermissionText(permission.getName(), permission.getAction(), permission.getId())); |
| | | func.put("id", permission.getAction()); |
| | | func.put("spread", true); |
| | | funcs.add(func); |
| | |
| | | return R.ok(result); |
| | | } |
| | | |
| | | private String localizeResourceName(Resource resource) { |
| | | return i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId()); |
| | | } |
| | | |
| | | @RequestMapping(value = "/power/{roleId}/auth") |
| | | @ManagerAuth |
| | | public R get(@PathVariable("roleId") Long roleId) { |
| | |
| | | import com.core.common.DateUtils; |
| | | import com.core.common.R; |
| | | import com.core.controller.AbstractBaseController; |
| | | import com.zy.common.i18n.I18nMessageService; |
| | | import com.zy.system.entity.Resource; |
| | | import com.zy.system.service.ResourceService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | |
| | | @Autowired |
| | | private ResourceService resourceService; |
| | | @Autowired |
| | | private I18nMessageService i18nMessageService; |
| | | |
| | | @RequestMapping(value = "/resource/{id}/auth") |
| | | @ManagerAuth |
| | |
| | | for (Resource resource : page.getRecords()){ |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("id", resource.getId()); |
| | | map.put("value", resource.getName().concat("(").concat(resource.getLevel$().substring(0, 2).concat(")"))); |
| | | String levelText = i18nMessageService.translateLegacy(Cools.isEmpty(resource.getLevel$()) ? "" : resource.getLevel$()); |
| | | String localizedName = i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId()); |
| | | String shortLevelText = levelText.matches(".*[\\u4E00-\\u9FA5].*") |
| | | ? levelText.substring(0, Math.min(2, levelText.length())) |
| | | : levelText; |
| | | map.put("value", localizedName.concat("(").concat(shortLevelText).concat(")")); |
| | | result.add(map); |
| | | } |
| | | return R.ok(result); |
| | |
| | | else { |
| | | wrapper.orderBy("sort"); |
| | | } |
| | | return R.parse("0-操作成功").add(resourceService.selectList(wrapper)); |
| | | List<Resource> resources = resourceService.selectList(wrapper); |
| | | for (Resource resource : resources) { |
| | | resource.setName(i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId())); |
| | | } |
| | | return R.parse("0-操作成功").add(resources); |
| | | } |
| | | |
| | | } |
| | |
| | | # 系统版本信息 |
| | | app: |
| | | version: 1.0.5.2 |
| | | version: 1.0.5.3 |
| | | version-type: dev # prd 或 dev |
| | | i18n: |
| | | default-locale: zh-CN |
| | | supported-locales: |
| | | - zh-CN |
| | | - en-US |
| | | # 内置语言包读取位置:classpath:/i18n/<locale>/*.properties |
| | | # 外置可安装语言包覆盖目录:./stock/out/@pom.build.finalName@/i18n/<locale>/*.properties |
| | | pack-path: ./stock/out/@pom.build.finalName@/i18n |
| | | # 外置语言包热加载检查周期(秒) |
| | | refresh-seconds: 10 |
| | | |
| | | server: |
| | | port: 9090 |
| New file |
| | |
| | | 账号=Account |
| | | 密码=Password |
| | | 登录=Sign In |
| | | 系统工具=System Tools |
| | | 推荐操作=Recommended Actions |
| | | 其他工具=Other Tools |
| | | 获取请求码=Get Request Code |
| | | 一键激活=Activate |
| | | 获取项目名称=Get Project Name |
| | | 获取系统配置=Get System Config |
| | | 录入许可证=Import License |
| | | 已复制到剪贴板=Copied to clipboard |
| | | 复制失败=Copy failed |
| | | 获取请求码失败=Failed to get request code |
| | | 获取系统配置信息失败=Failed to get system configuration |
| | | 许可证内容不能为空=License content cannot be empty |
| | | 许可证更新成功=License updated successfully |
| | | 许可证更新失败=Failed to update license |
| | | 许可证录入失败=Failed to import license |
| | | 激活成功=Activation successful |
| | | 激活失败=Activation failed |
| | | 获取项目名称失败=Failed to get project name |
| | | 请输入账号=Please enter account |
| | | 请输入密码=Please enter password |
| | | 搜索菜单=Search menu |
| | | 没有匹配菜单=No matching menus |
| | | 当前账号没有可用菜单=No available menus |
| | | 临时许可证有效期:=Temporary license valid: |
| | | 仿真运行中=Simulation Running |
| | | 仿真未运行=Simulation Stopped |
| | | 基本资料=Profile |
| | | 退出登录=Log Out |
| | | 关闭其他页签=Close Other Tabs |
| | | 返回控制中心=Back to Dashboard |
| | | 许可证即将过期=License Expiring Soon |
| | | 知道了=OK |
| | | 控制中心=Control Center |
| | | 实时监控=Real-time Monitoring |
| | | 账户中心=Account Center |
| | | 管理员=Admin |
| | | 正在加载页面...=Loading page... |
| | | AI助手=AI Assistant |
| | | 确定要停止仿真模拟吗?=Are you sure you want to stop the simulation? |
| | | 确定要启动仿真模拟吗?=Are you sure you want to start the simulation? |
| | | 仿真模拟已停止=Simulation stopped |
| | | 仿真模拟已启动=Simulation started |
| | | 操作失败=Operation failed |
| | | 菜单加载失败=Failed to load menu |
| | | 菜单加载失败,请检查接口状态=Failed to load menu. Please check the API status. |
| | | 工作页面=Work Page |
| | | 业务页面=Business Page |
| | | 编号=ID |
| | | 起始时间 - 终止时间=Start time - End time |
| | | 请输入=Please enter |
| | | 请输入...=Please enter... |
| | | 请选择数据=Please select data |
| | | 请选择要删除的数据=Please select data to delete |
| | | 无数据=No data |
| | | 已存在=Already exists |
| | | 不可用=Unavailable |
| | | 取消选择=Clear selection |
| | | 正常=Active |
| | | 禁用=Disabled |
| | | 启用=Enabled |
| | | 冻结=Frozen |
| | | 删除=Delete |
| | | 一级菜单=Level 1 Menu |
| | | 二级菜单=Level 2 Menu |
| | | 三级菜单=Level 3 Menu |
| | | 查询=Search |
| | | 重置=Reset |
| | | 新增=Add |
| | | 编辑=Edit |
| | | 修改=Edit |
| | | 导出=Export |
| | | 保存=Save |
| | | 取消=Cancel |
| | | 返回=Back |
| | | 详情=Details |
| | | 工作号=Work No. |
| | | WMS工作号=WMS Work No. |
| | | 源库位=Source Location |
| | | 目标库位=Target Location |
| | | 堆垛机=Crane |
| | | 双工位堆垛机=Dual-station Crane |
| | | 区域编码=Area Code |
| | | 未命名页面=Untitled Page |
| | | 未命名分组=Untitled Group |
| | | |
| | | # Generic controls |
| | | 操作=Actions |
| | | 打印=Print |
| | | 搜索=Search |
| | | 重置=Reset |
| | | 筛选列=Columns |
| | | 到第=Go to |
| | | 确定=OK |
| | | 编号=ID |
| | | 导出=Export |
| | | 起始时间 - 终止时间=Start Time - End Time |
| | | 修改人员=Updated By |
| | | 修改时间=Updated At |
| | | 备注=Remarks |
| | | 新增=Add |
| | | 工作号=Work No. |
| | | 降序=Descending |
| | | 升序=Ascending |
| | | 添加时间=Created At |
| | | 修改=Edit |
| | | 状态=Status |
| | | 目标库位=Target Location |
| | | 请选择=Please select |
| | | 无数据=No data |
| | | 尾页=Last Page |
| | | 正常=Active |
| | | 添加人员=Created By |
| | | 异常=Exception |
| | | 异常码=Error Code |
| | | 工作状态=Work Status |
| | | 没有选项=No options |
| | | 目标站=Target Station |
| | | 源站=Source Station |
| | | 堆垛机号=Crane No. |
| | | 命令=Command |
| | | 请求响应=Request/Response |
| | | 入出库类型=IO Type |
| | | 设备编号=Device No. |
| | | 条码=Barcode |
| | | 系统状态=System Status |
| | | 下发时间=Issued At |
| | | 下发状态=Issue Status |
| | | 作业=Operation |
| | | 创建时间=Created At |
| | | 发生时间=Occurred At |
| | | 结果=Result |
| | | 结束时间=Ended At |
| | | 类型=Type |
| | | 名称=Name |
| | | 起点库位=Source Location |
| | | 刷新=Refresh |
| | | 已下发=Issued |
| | | 保存=Save |
| | | 编码=Code |
| | | 成功=Success |
| | | 角色=Role |
| | | 接口地址=API Endpoint |
| | | 批次=Batch |
| | | 批次序列=Batch Sequence |
| | | 前往=Go |
| | | 设备类型=Device Type |
| | | 失败=Failed |
| | | 详情=Details |
| | | 用户=User |
| | | 优先级=Priority |
| | | 暂无数据=No Data |
| | | 注册时间=Registered At |
| | | 自动=Auto |
| | | RGV号=RGV No. |
| | | 关闭=Close |
| | | 工作时间=Work Time |
| | | 托盘码=Pallet Code |
| | | 源站点=Source Station |
| | | 目标站点=Target Station |
| | | 设备ip=Device IP |
| | | 设备端口=Device Port |
| | | 网关编号=Gateway No. |
| | | 实现类=Implementation Class |
| | | 虚拟设备=Virtual Device |
| | | 创建人员=Created By |
| | | 请求时间=Request Time |
| | | 响应参数=Response Payload |
| | | 请求参数=Request Payload |
| | | 请求内容=Request Content |
| | | 响应内容=Response Content |
| | | 名称空间=Namespace |
| | | 登录账户=Login Account |
| | | 手机号=Phone |
| | | 邮箱=Email |
| | | 确认修改=Confirm Update |
| | | 设置我的资料=Edit My Profile |
| | | 不可修改=Not Editable |
| | | 重要!一般用于后台登入=Important: usually used for admin login |
| | | 当前角色不可更改为其它角色=Current role cannot be changed |
| | | 手机号:=Phone: |
| | | 用户名:=Username: |
| | | 输入手机号=Enter phone number |
| | | 输入用户名=Enter username |
| | | 重置密码=Reset Password |
| | | 首页菜单=Home Menu |
| | | 客户端IP=Client IP |
| | | 请求数据=Request Data |
| | | 响应数据=Response Data |
| | | 操作内容=Operation Content |
| | | 凭证值=Credential Value |
| | | 员工=Employee |
| | | 菜单=Menu |
| | | 按钮=Button |
| | | 添加=Add |
| | | 菜单编码=Menu Code |
| | | 菜单名称=Menu Name |
| | | 权限名称=Permission Name |
| | | 所属菜单=Menu |
| | | 权限=Permissions |
| | | 上级=Parent |
| | | 账号=Account |
| | | 密码=Password |
| | | |
| | | # Common dynamic patterns |
| | | regex\:^(\\d+)\\s*条/页$=$1 / page |
| | | regex\:^(\\d+)条/页$=$1 / page |
| | | regex\:^共\\s*(\\d+)\\s*条$=Total $1 items |
| | | regex\:^共\\s*(\\d+)\\s*个设备$=Total $1 devices |
| | | regex\:^(\\d+)号堆垛机$=Crane $1 |
| | | regex\:^(\\d+)号双工位堆垛机$=Dual-station Crane $1 |
| | | regex\:^堆垛机\\s*(\\d+)$=Crane $1 |
| | | regex\:^双工位堆垛机\\s*(\\d+)$=Dual-station Crane $1 |
| | | regex\:^站点列表\\s*\\((\\d+)\\)$=Station List ($1) |
| | | regex\:^堆垛机设备\\s*\\((\\d+)\\)$=Crane Devices ($1) |
| | | regex\:^出库区域\\s*\\((\\d+)\\)$=Outbound Areas ($1) |
| | | regex\:^出库站点\\s*\\((\\d+)\\)$=Outbound Stations ($1) |
| | | regex\:^工作号\\s*(\\d+)\\s*\\|\\s*目标\\s*(.+)$=Work No. $1 | Target $2 |
| | | regex\:^A(\\d+)\\s*-\\s*月台(\\d+)$=A$1 - Dock $2 |
| | | |
| | | # Watch and map pages |
| | | 地图操作=Map Controls |
| | | 重置视图=Reset View |
| | | 站点颜色=Station Color |
| | | 收起面板=Collapse Panel |
| | | 监控工作台=Monitoring Workbench |
| | | 堆垛机监控=Crane Monitor |
| | | 双工位=Dual Station |
| | | 输送站=Stations |
| | | 上一页=Previous |
| | | 下一页=Next |
| | | 打开控制中心=Open Control Center |
| | | 楼层=Floor |
| | | 旋转=Rotate |
| | | 镜像=Mirror |
| | | 显示站点方向=Show Station Direction |
| | | 库位地图=Location Map |
| | | 点击库位后在右侧查看详情。=Click a location to view details on the right. |
| | | 点击库位可查看库位状态,点击站点可查看站点作业详情。地图操作在右上角工具面板中。=Click a location to view its status, or a station to view job details. Use the top-right tool panel for map controls. |
| | | 货架=Shelf |
| | | 实时数据=Live Data |
| | | 原始地图=Raw Map |
| | | 最近数据=Latest Data |
| | | 初始化库位=Initialize Locations |
| | | 导入地图=Import Map |
| | | |
| | | # Work pages |
| | | 任务管理=Task Management |
| | | 任务类型=Task Type |
| | | 系统消息=System Message |
| | | 源库位=Source Location |
| | | 时间=Time |
| | | |
| | | # Baseline data and enums |
| | | 1.入库=1. Inbound |
| | | 101.出库=101. Outbound |
| | | 201.移库任务=201. Transfer |
| | | 1.生成入库任务=1. Create Inbound Task |
| | | 2.设备上走=2. Device Moving Up |
| | | 3.设备搬运中=3. Device Handling |
| | | 9.入库完成=9. Inbound Completed |
| | | 109.出库完成=109. Outbound Completed |
| | | F.在库=F. In Stock |
| | | X.禁用=X. Disabled |
| | | O.空库位=O. Empty Location |
| | | C.充电占用=C. Charging Occupied |
| | | R.出库预约=R. Outbound Reserved |
| | | S.入库预约=S. Inbound Reserved |
| | | 出库站列表=Outbound Stations |
| | | 入库站列表=Inbound Stations |
| | | 深库位排号=Deep Location Row No. |
| | | 控制库位排号=Control Location Row No. |
| | | 站点数据=Station Data |
| | | 顶升移栽点=Lift Transfer Point |
| | | 出库站点数据=Outbound Station Data |
| | | 入库站点数据=Inbound Station Data |
| | | 站点别名=Station Alias |
| | | 站点楼层=Station Floor |
| | | 库位状态代号=Location Status Code |
| | | 库位状态描述=Location Status Description |
| | | 状态描述=Status Description |
| | | 入出类型代号=IO Type Code |
| | | 入出类型描述=IO Type Description |
| | | 当前ID=Current ID |
| | | 启动入库=Start Inbound |
| | | 起始ID=Start ID |
| | | 终止ID=End ID |
| | | 站点回退=Station Rollback |
| | | 移库任务=Transfer Task |
| | | 仿真随机工作号=Simulated Random Work No. |
| | | 低库位=Low Location |
| | | 高库位=High Location |
| | | 库位号=Location Code |
| | | 宽库位=Wide Location |
| | | 起止层=Start/End Level |
| | | 起止列=Start/End Column |
| | | 起止排=Start/End Row |
| | | 轻库位=Light Location |
| | | 窄库位=Narrow Location |
| | | 重库位=Heavy Location |
| | | 高低类型=High/Low Type |
| | | 库位类型=Location Type |
| | | |
| | | # Station and area configuration |
| | | 站点设备关系配置=Station-Device Mapping |
| | | 站点与堆垛机任务关联配置=Station-Crane Task Mapping |
| | | 保存配置=Save Configuration |
| | | 操作说明:拖拽左侧的【站点】到右侧的【设备】上即可建立关联。点击连线可删除关联。=Drag a station from the left to a device on the right to create a mapping. Click a link to remove it. |
| | | 出库站与出库区域绑定配置=Outbound Station-Area Binding |
| | | 区域名称=Area Name |
| | | 新增区域=Add Area |
| | | 操作说明:拖拽左侧【出库站点】到右侧【出库区域】上建立绑定。点击连线可删除绑定。=Drag an outbound station from the left to an outbound area on the right to create a binding. Click a link to remove the binding. |
| | | |
| | | # Notification report |
| | | 通知上报=Notification Report |
| | | 补发=Retry |
| | | 任务=Task |
| | | 任务号=Task No. |
| | | 设备号=Device No. |
| | | 输送线=Conveyor |
| | | 已开启=Enabled |
| | | 查看报文=View Request |
| | | 查看响应=View Response |
| | | 队列状态=Queue Status |
| | | 发送时间=Sent At |
| | | 批量补发=Retry Selected |
| | | 任务完成=Task Completed |
| | | 日志结果=Log Result |
| | | 筛选条件=Filters |
| | | 上报开关=Reporting Switch |
| | | 刷新全局=Refresh Global |
| | | |
| | | # Device logs |
| | | 设备日志=Device Logs |
| | | 全部=All |
| | | 下载=Download |
| | | 日期选择=Date Selection |
| | | 设备列表=Device List |
| | | 选中日期=Selected Date |
| | | 起始序号=Start Index |
| | | 最大文件=Max Files |
| | | 请输入编号=Enter ID |
| | | 文件下载中=Downloading file |
| | | 日志可视化 - ()=Device Logs - () |
| | | 暂无数据,请先选择日期=No data. Please select a date first. |
| | | |
| | | # Debug and system config |
| | | 调试参数=Debug Parameters |
| | | 充电参数=Charging Settings |
| | | 调度参数=Dispatch Settings |
| | | 避障内圈半径=Inner Avoidance Radius |
| | | 避障外圈半径=Outer Avoidance Radius |
| | | 定时充电开关=Scheduled Charging |
| | | 入库预留小车=Reserved Inbound AGV |
| | | 小车满电校准=Full Battery Calibration |
| | | 演示模式参数=Demo Mode Settings |
| | | 定时充电时间段=Scheduled Charging Window |
| | | 小车定时充电线=Scheduled Charging Line |
| | | 小车默认充电线=Default Charging Line |
| | | 演示模式-跑库=Demo Mode - Cycle Storage |
| | | 小车充电最大阈值=Max Charge Threshold |
| | | 小车电量预警阈值=Low Battery Threshold |
| | | 输出RCS调试日志=Output RCS Debug Logs |
| | | 演示模式-货物搬运=Demo Mode - Cargo Handling |
| | | 移动演示模式-楼层=Moving Demo Mode - Floor |
| | | 调度小车同层最大数量=Max AGVs Per Floor |
| | | 小车出提升机近点距离=AGV Lift Exit Distance |
| | | 小车移动连续下发指令=Continuous AGV Move Commands |
| | | 允许交管重新规划路径=Allow Traffic Replan |
| | | 地图母轨方向(x,y)=Map Main Rail Direction (x,y) |
| | | 移动演示模式-是否换层=Moving Demo Mode - Change Floor |
| | | 小车(x,y)命令运行方向颠倒=Invert AGV (x,y) Command Direction |
| | | 对应值=Value |
| | | 筛选类型=Filter Type |
| | | 刷新缓存=Refresh Cache |
| | | 出库迟到惩罚=Outbound Delay Penalty |
| | | 监控地图镜像=Monitor Map Mirror |
| | | 监控地图旋转=Monitor Map Rotation |
| | | |
| | | # AI page |
| | | AI配置=AI Configuration |
| | | 必填=Required |
| | | 测试=Test |
| | | 复制=Copy |
| | | 更多=More |
| | | 模型=Model |
| | | 启用=Enabled |
| | | 思考=Thinking |
| | | 冷却中=Cooling |
| | | 清冷却=Clear Cooldown |
| | | 总路由=Default Route |
| | | 调用日志=Call Logs |
| | | 额度切换=Quota Switch |
| | | 复制全文=Copy All |
| | | 故障切换=Failover |
| | | 冷却秒数=Cooldown Seconds |
| | | 路由名称=Route Name |
| | | 日志详情=Log Details |
| | | 新增路由=Add Route |
| | | 导出JSON=Export JSON |
| | | 导入JSON=Import JSON |
| | | |
| | | # Second-pass audit fixes |
| | | 查询=Query |
| | | 出库=Outbound |
| | | 入库=Inbound |
| | | 代号=Code |
| | | 排序=Sort |
| | | 筛选=Filter |
| | | 当前没有可展示的堆垛机数据=No crane data available |
| | | 当前没有待发送通知=No pending notifications |
| | | 双伸位堆垛机=Dual-reach Crane |
| | | 双伸位Crane=Dual-reach Crane |
| | | 通知地址=Notify URL |
| | | 通知ID=Notify ID |
| | | 消息描述=Message Description |
| | | 重试次数=Retry Count |
| | | 当前队列数=Current Queue Size |
| | | 间隔(s)=Interval (s) |
| | | 通知日志数=Notification Log Count |
| | | 当前通知队列=Current Notification Queue |
| | | 通知发送日志=Notification Send Logs |
| | | 通知类型=Notification Type |
| | | 通知Type=Notification Type |
| | | 上次重试时间=Last Retry Time |
| | | 上次重试Time=Last Retry Time |
| | | 消息类型/描述=Message Type / Description |
| | | 消息Type/描述=Message Type / Description |
| | | 通知查看与补发=Notification View & Retry |
| | | 通知查看与Retry=Notification View & Retry |
| | | 通知接口历史调用记录=Notification API History |
| | | 展示待发送和待重试通知=Show pending and retry notifications |
| | | 支持从历史日志重新发送通知=Support resending from history |
| | | 已选 0 条=0 selected |
| | | 站点ID=Station ID |
| | | 通知上报=Notification Report |
| | | 当前页签:通知队列=Current Tab: Notification Queue |
| | | 通知队列=Notification Queue |
| | | 通知日志=Notification Logs |
| | | 任务类型/描述=Task Type / Description |
| | | 消息类型=Message Type |
| | | 消息Type=Message Type |
| | | 通知类型/描述=Notification Type / Description |
| | | 设备日志=Device Logs |
| | | 日志可视化 - ()=Device Logs - () |
| | | 日志可视化=Device Logs |
| | | 日期选择=Date Selection |
| | | 选中日期=Selected Date |
| | | 起始序号=Start Index |
| | | 最大文件=Max Files |
| | | 文件下载中=Downloading file |
| | | 设备列表=Device List |
| | | 设备号=Device No. |
| | | 当前没有待发送通知=No notifications to send |
| | | 03月=03 |
| | | 03日=03 |
| | | 04日=04 |
| | | 05日=05 |
| | | 06日=06 |
| | | 07日=07 |
| | | 08日=08 |
| | | 09日=09 |
| | | 未知=Unknown |
| | | 宽窄类型=Width Type |
| | | 宽窄Type=Width Type |
| | | 轻重类型=Weight Type |
| | | 轻重Type=Weight Type |
| | | 堆垛机数量=Crane Count |
| | | Crane数量=Crane Count |
| | | 库位状态=Location Status |
| | | 库位Status=Location Status |
| | | 层数=Levels |
| | | 更新时间=Updated At |
| | | 更新Time=Updated At |
| | | 系统运行状态=System Runtime Status |
| | | 系统运行Status=System Runtime Status |
| | | 启动仿真模拟=Start Simulation |
| | | 停止仿真模拟=Stop Simulation |
| | | 异常情况=Exception Details |
| | | Exception情况=Exception Details |
| | | 系统状态数据=System Status Payload |
| | | System Status数据=System Status Payload |
| | | 最大出库任务数=Max Outbound Tasks |
| | | 最大出库Task数=Max Outbound Tasks |
| | | 最大入库任务数=Max Inbound Tasks |
| | | 最大入库Task数=Max Inbound Tasks |
| | | 可出(checkBox)=Outbound Enabled (Checkbox) |
| | | 可入(checkBox)=Inbound Enabled (Checkbox) |
| | | 工位1禁止执行列=Station 1 Disabled Columns |
| | | 工位2禁止执行列=Station 2 Disabled Columns |
| | | 出库站点=Outbound Stations |
| | | 入库站点=Inbound Stations |
| | | 出库排序交互点=Outbound Sort Interaction Point |
| | | 初始化站点数据=Initialize Station Data |
| | | 初始化Station Data=Initialize Station Data |
| | | 运行堵塞重新分配库位站点数据=Reassign blocked location station data |
| | | 运行堵塞重新分配库位Station Data=Reassign blocked location station data |
| | | 虚拟设备初始化设备状态=Virtual Device Initializes Device Status |
| | | Virtual Device初始化设备Status=Virtual Device Initializes Device Status |
| | | Initialize Locations后将Delete库存明细,请谨慎Actions!=Initializing locations will delete inventory details. Proceed carefully. |
| | | 绕圈最大承载量=Max Loop Load |
| | | 完工时间权重=Finish Time Weight |
| | | 完工Time权重=Finish Time Weight |
| | | 并行搜索线程数=Parallel Search Threads |
| | | 并行Search线程数=Parallel Search Threads |
| | | 最大求解时间(s)=Max Solve Time (s) |
| | | 最大求解Time(s)=Max Solve Time (s) |
| | | 任务等待时间权重=Task Wait Time Weight |
| | | Task等待Time权重=Task Wait Time Weight |
| | | 是否启用出库节拍=Enable Outbound Rhythm |
| | | 是否Enabled出库节拍=Enable Outbound Rhythm |
| | | 优先级早完成权重=Priority Early Completion Weight |
| | | Priority早完成权重=Priority Early Completion Weight |
| | | 站点点绕圈模式=Station Loop Mode |
| | | Stations点绕圈模式=Station Loop Mode |
| | | 同一出库组序列之间的最小间隔=Min Interval Between Same Outbound Group Sequences |
| | | 堆垛机货叉放货耗时(s)=Crane Fork Putaway Time (s) |
| | | Crane货叉放货耗时(s)=Crane Fork Putaway Time (s) |
| | | 堆垛机货叉取货耗时(s)=Crane Fork Pickup Time (s) |
| | | Crane货叉取货耗时(s)=Crane Fork Pickup Time (s) |
| | | 输送线命令分段长度=Conveyor Command Segment Length |
| | | ConveyorCommand分段长度=Conveyor Command Segment Length |
| | | 站点点最大任务数量上限=Max Tasks Per Station |
| | | Stations点最大Task数量上限=Max Tasks Per Station |
| | | 冷却到: -=Cooldown Until: - |
| | | 最近错误: -=Latest Error: - |
| | | 故障切换开启=Failover Enabled |
| | | Failover开启=Failover Enabled |
| | | 额度切换开启=Quota Switch Enabled |
| | | Quota Switch开启=Quota Switch Enabled |
| | | 优先级(越小越优先)=Priority (smaller is higher) |
| | | AI配置 - LLM路由=AI Configuration - LLM Routes |
| | | 支持多API、多模型、多Key,额度耗尽或故障自动切换=Supports multiple APIs, models, and keys with automatic failover on quota exhaustion or errors |
| | | 支持多API、多Model、多Key,额度耗尽或故障Auto切换=Supports multiple APIs, models, and keys with automatic failover on quota exhaustion or errors |
| | | 成功 0 / 失败 0 / 连续失败 0=Success 0 / Failed 0 / Consecutive Failures 0 |
| | | Success 0 / Failed 0 / 连续Failed 0=Success 0 / Failed 0 / Consecutive Failures 0 |
| | | Success 7 / Failed 2 / 连续Failed 0=Success 7 / Failed 2 / Consecutive Failures 0 |
| | | Success 8 / Failed 0 / 连续Failed 0=Success 8 / Failed 0 / Consecutive Failures 0 |
| | | 必填,例如: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | Required,例如: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1 |
| | | 最近错误:=Latest Error: |
| | | 冷却到:=Cooldown Until: |
| | | D.空桶/空栈板=D. Empty Tote/Pallet |
| | | E.出入专用轨道=E. Dedicated IO Rail |
| | | W.穿梭车母轨道=W. Shuttle Main Rail |
| | | P.拣料/盘点/并板出库中=P. Picking/Counting/Merging Outbound |
| | | Q.拣料/盘点/并板再入库=Q. Picking/Counting/Merging Re-Inbound |
| | | 4.设备搬运完成=4. Device Handling Completed |
| | | 10.库存更新完成=10. Inventory Update Completed |
| | | 102.设备搬运中=102. Device Handling In Progress |
| | | 104.站点运行中=104. Station Running |
| | | 502.设备搬运中=502. Device Handling In Progress |
| | | 103.设备搬运完成=103. Device Handling Completed |
| | | 110.库存更新完成=110. Inventory Update Completed |
| | | 503.设备搬运完成=503. Device Handling Completed |
| | | 509.移库完成=509. Transfer Completed |
| | | 101.生成出库任务=101. Create Outbound Task |
| | | 101.生成出库Task=101. Create Outbound Task |
| | | 501.生成移库任务=501. Create Transfer Task |
| | | 501.生成Transfer Task=501. Create Transfer Task |
| | | |
| | | 页=page |
| | | 排=Row |
| | | 列=Column |
| | | 层=Level |
| | | 是=Yes |
| | | 否=No |
| | | 开=On |
| | | 关=Off |
| | | 工作数据不存在=Work data does not exist |
| | | 已受理(异步执行)=Accepted (async) |
| | | Command已受理(异步执行)=Command accepted (async) |
| | | 通知上报中心=Notification Report Center |
| | | 查看当前待通知队列、接口发送日志,支持按任务/设备快速筛选,并对失败或待发送通知执行手动补发。=View pending notification queues and send logs, filter quickly by task or device, and manually resend failed or pending notifications. |
| | | Redis 中待上报或待重试通知=Notifications pending report or retry in Redis |
| | | 来源:notifyUri + notifyUriPath=Source: notifyUri + notifyUriPath |
| | | 队列与日志共用同一组查询条件,切换页签时保持一致。=Queue and log tabs share the same query conditions and stay in sync when switching tabs. |
| | | 通用搜索:任务号、消息描述、报文关键字、Redis Key=General search: Task No., message description, payload keyword, Redis key |
| | | 当前队列显示 Redis 实时数据,发送日志显示历史接口调用结果。=The current queue shows live Redis data, and send logs show historical API call results. |
| | | CraneInboundTask执行中=CraneInboundTask in progress |
| | | LLM调用日志=LLM Call Logs |
| | | 日志详情 - =Log Details - |
| | | 日志可视化 - =Device Logs - |
| | | 信息=Info |
| | | 确定导出Excel吗=Export to Excel? |
| | | 确定修改资料吗?=Confirm profile update? |
| | | 修改密码=Change Password |
| | | 当前密码=Current Password |
| | | 新密码=New Password |
| | | 确认新密码=Confirm New Password |
| | | 密码不匹配=Password does not match |
| | | 新密码不能为空=New password cannot be empty |
| | | 不能少于4个字符=Must be at least 4 characters |
| | | 与旧密码不能相同=Must differ from the current password |
| | | 密码不一致=Passwords do not match |
| | | 密码修改成功,请重新登录=Password updated successfully. Please sign in again. |
| | | 收起操作=Hide Controls |
| | | 隐藏站点方向=Hide Station Direction |
| | | 取消镜像=Disable Mirror |
| | | 展开面板=Expand Panel |
| | | 收起控制中心=Hide Control Center |
| | | 双工位堆垛机监控=Dual-station Crane Monitor |
| | | 输送监控=Station Monitor |
| | | RGV监控=RGV Monitor |
| | | 请输入堆垛机号=Enter crane number |
| | | 请输入RGV号=Enter RGV number |
| | | 请输入站号=Enter station number |
| | | 堆垛机号=Crane No. |
| | | RGV号=RGV No. |
| | | 站号=Station No. |
| | | 源点=Source Point |
| | | 目标点=Target Point |
| | | 输入源点=Enter source point |
| | | 输入目标点=Enter target point |
| | | 输入工作号=Enter work number |
| | | 输入目标站号=Enter target station no. |
| | | 取放货=Pick/Put |
| | | 移动=Move |
| | | 任务完成=Task Complete |
| | | 下发=Send |
| | | 复位=Reset |
| | | 编号=ID |
| | | 模式=Mode |
| | | 状态=Status |
| | | 是否有物=Loaded |
| | | 任务接收=Task Receive |
| | | 货叉定位=Fork Position |
| | | 载货台定位=Lift Position |
| | | 走行定位=Travel Position |
| | | 走行速度=Travel Speed |
| | | 升降速度=Lift Speed |
| | | 叉牙速度=Fork Speed |
| | | 称重数据=Weight |
| | | 条码数据=Barcode |
| | | 故障代码=Error Code |
| | | 故障描述=Alarm Description |
| | | 扩展数据=Extended Data |
| | | 当前没有可展示的堆垛机数据=No crane data available |
| | | 工位=Station |
| | | 工位1=Station 1 |
| | | 工位2=Station 2 |
| | | 编辑工位1任务号=Edit Station 1 Task No. |
| | | 编辑工位2任务号=Edit Station 2 Task No. |
| | | 当前没有可展示的双工位堆垛机数据=No dual-station crane data available |
| | | 异常码=Error Code |
| | | 工位1任务号=Station 1 Task No. |
| | | 工位2任务号=Station 2 Task No. |
| | | 设备工位1任务号=Device Station 1 Task No. |
| | | 设备工位2任务号=Device Station 2 Task No. |
| | | 工位1状态=Station 1 Status |
| | | 工位2状态=Station 2 Status |
| | | 工位1是否有物=Station 1 Loaded |
| | | 工位2是否有物=Station 2 Loaded |
| | | 工位1货叉定位=Station 1 Fork Position |
| | | 工位2货叉定位=Station 2 Fork Position |
| | | 工位1任务接收=Station 1 Task Receive |
| | | 工位2任务接收=Station 2 Task Receive |
| | | 工位1下发数据=Station 1 Sent Data |
| | | 工位2下发数据=Station 2 Sent Data |
| | | 站=Station |
| | | 任务=Task |
| | | 手动=Manual |
| | | 有物=Loaded |
| | | 可入=Inbound Enabled |
| | | 可出=Outbound Enabled |
| | | 空板信号=Empty Pallet Signal |
| | | 满板信号=Full Pallet Signal |
| | | 运行阻塞=Run Block |
| | | 托盘高度=Pallet Height |
| | | 条码=Barcode |
| | | 重量=Weight |
| | | 任务可写区=Task Writable Area |
| | | 故障信息=Error Message |
| | | 当前没有可展示的站点数据=No station data available |
| | | 例如 1=For example: 1 |
| | | 例如 2=For example: 2 |
| | | 例如 101=For example: 101 |
| | | 当前没有可展示的RGV数据=No RGV data available |
| | | 轨道位=Track Position |
| | | 库位详情=Location Details |
| | | 站点信息=Station Info |
| | | 站点作业状态、来源目标和作业参数=Station job status, source/target, and job parameters |
| | | 库位当前状态和所在排列层信息=Current location status and row/bay/level details |
| | | 站点颜色配置=Station Color Configuration |
| | | 暂无站点颜色配置项=No station color configuration items |
| | | regex\:^(\\d+)站$=Station $1 |
| | | regex\:^任务\\s*(.+)\\s*\\|\\s*目标站\\s*(.+)$=Task $1 | Target Station $2 |
| | | regex\:^轨道位\\s*(.+)\\s*\\|\\s*任务\\s*(.+)$=Track $1 | Task $2 |
| | | regex\:^工位1\\s*(.+)\\s*\\|\\s*工位2\\s*(.+)$=Station 1 $1 | Station 2 $2 |
| | | regex\:^(\\d+)号RGV$=RGV $1 |
| | | 15条/页=15 / page |
| | | 30条/页=30 / page |
| | | 50条/页=50 / page |
| | | 100条/页=100 / page |
| | | 200条/页=200 / page |
| | | 500条/页=500 / page |
| | | Stations点绕圈Mode=Station Loop Mode |
| | | 阿里百炼-MiniMax-M2.1=Alibaba Bailian - MiniMax-M2.1 |
| | | 阿里百炼-MiniMax-M2.5=Alibaba Bailian - MiniMax-M2.5 |
| | | 阿里百炼-qwen3.5-flash=Alibaba Bailian - qwen3.5-flash |
| | | 阿里百炼-qwen3.5-plus=Alibaba Bailian - qwen3.5-plus |
| | | 阿里百炼-kimi-k2.5=Alibaba Bailian - kimi-k2.5 |
| | | 阿里百炼-glm-5=Alibaba Bailian - glm-5 |
| | | 硅基流动=SiliconFlow |
| New file |
| | |
| | | lang.zh-CN=Chinese (Simplified) |
| | | lang.en-US=English |
| | | app.title=Zhejiang Zhongyang - Automated AS/RS - WCS |
| | | app.company=Zhejiang Zhongyang Warehouse Technology Co., Ltd. |
| | | common.loadingPage=Loading page... |
| | | common.loadingTab=Loading "{0}" ... |
| | | common.refreshingTab=Refreshing "{0}" ... |
| | | common.ok=OK |
| | | common.prompt=Prompt |
| | | common.language=Language |
| | | common.profile=Profile |
| | | common.logout=Log Out |
| | | common.closeOtherTabs=Close Other Tabs |
| | | common.backHome=Back to Dashboard |
| | | common.aiAssistant=AI Assistant |
| | | common.workPage=Work Page |
| | | common.businessPage=Business Page |
| | | login.title=WCS System V3.0 |
| | | login.username=Account |
| | | login.password=Password |
| | | login.submit=Sign In |
| | | login.tools.title=System Tools |
| | | login.tools.recommended=Recommended Actions |
| | | login.tools.recommendedDesc=Use "Get Request Code" and "Activate" first to complete license application and activation. |
| | | login.tools.others=Other Tools |
| | | login.tools.requestCode=Get Request Code |
| | | login.tools.activate=Activate |
| | | login.tools.projectName=Get Project Name |
| | | login.tools.serverInfo=Get System Config |
| | | login.tools.uploadLicense=Import License |
| | | login.dialog.copy=Copy |
| | | login.dialog.copied=Copied to clipboard |
| | | login.dialog.copyFailed=Copy failed |
| | | login.requestCode.title=Request Code |
| | | login.requestCode.label=Request Code |
| | | login.requestCode.tip=The request code already contains the project name and can be sent directly to the license service. |
| | | login.serverInfo.title=System Configuration |
| | | login.serverInfo.label=System Configuration |
| | | login.serverInfo.tip=Legacy projects can still use this hardware JSON to apply for a license. |
| | | index.searchMenu=Search menu |
| | | index.noMatchedMenu=No matching menus |
| | | index.noAvailableMenu=No available menus for current account |
| | | index.licenseDays=Temporary license valid: {0} day(s) |
| | | index.fakeRunning=Simulation Running |
| | | index.fakeStopped=Simulation Stopped |
| | | index.licenseExpiring=License Expiring Soon |
| | | index.homeTab=Control Center |
| | | index.homeGroup=Real-time Monitoring |
| | | index.profileGroup=Account Center |
| | | index.versionLoading=Version loading... |
| | | index.licenseExpireAt=The license will expire on {0}. Remaining validity: {1} day(s). |
| | | index.confirmStopFake=Are you sure you want to stop the simulation? |
| | | index.confirmStartFake=Are you sure you want to start the simulation? |
| | | index.fakeStoppedSuccess=Simulation stopped |
| | | index.fakeStartedSuccess=Simulation started |
| | | index.operationFailed=Operation failed |
| | | index.menuLoadFailed=Failed to load menu |
| | | index.menuLoadFailedDetail=Failed to load menu. Please check the API status. |
| | | response.user.notFound=Account does not exist |
| | | response.user.disabled=Account is disabled |
| | | response.user.passwordMismatch=Incorrect password |
| | | response.system.licenseExpired=License has expired |
| | | response.common.systemError=System error. Please try again later. |
| | | response.common.methodNotAllowed=Request method not supported |
| | | resource.index=Control Center |
| | | resource.system=System Management |
| | | resource.set=System Settings |
| | | resource.merchant=Customer Management |
| | | resource.develop=Development |
| | | resource.stock=Inventory |
| | | resource.logReport=Logs & Reports |
| | | resource.ioWork=Inbound/Outbound Jobs |
| | | resource.workFlow=Workflow |
| | | resource.base=Basic Data |
| | | resource.erp=ERP Integration |
| | | resource.sensor=Sensor Devices |
| | | resource.ai.llm_config=AI Configuration |
| | | resource.notifyReport.notifyReport=Notification Report |
| | | resource.view=View |
| | | permission.function=Specified Functions |
| | | el.colorpicker.confirm=OK |
| | | el.colorpicker.clear=Clear |
| | | el.datepicker.now=Now |
| | | el.datepicker.today=Today |
| | | el.datepicker.cancel=Cancel |
| | | el.datepicker.clear=Clear |
| | | el.datepicker.confirm=OK |
| | | el.datepicker.selectDate=Select date |
| | | el.datepicker.selectTime=Select time |
| | | el.datepicker.startDate=Start date |
| | | el.datepicker.startTime=Start time |
| | | el.datepicker.endDate=End date |
| | | el.datepicker.endTime=End time |
| | | el.datepicker.prevYear=Previous year |
| | | el.datepicker.nextYear=Next year |
| | | el.datepicker.prevMonth=Previous month |
| | | el.datepicker.nextMonth=Next month |
| | | el.datepicker.year= |
| | | el.datepicker.month1=January |
| | | el.datepicker.month2=February |
| | | el.datepicker.month3=March |
| | | el.datepicker.month4=April |
| | | el.datepicker.month5=May |
| | | el.datepicker.month6=June |
| | | el.datepicker.month7=July |
| | | el.datepicker.month8=August |
| | | el.datepicker.month9=September |
| | | el.datepicker.month10=October |
| | | el.datepicker.month11=November |
| | | el.datepicker.month12=December |
| | | el.datepicker.weeks.sun=Sun |
| | | el.datepicker.weeks.mon=Mon |
| | | el.datepicker.weeks.tue=Tue |
| | | el.datepicker.weeks.wed=Wed |
| | | el.datepicker.weeks.thu=Thu |
| | | el.datepicker.weeks.fri=Fri |
| | | el.datepicker.weeks.sat=Sat |
| | | el.datepicker.months.jan=Jan |
| | | el.datepicker.months.feb=Feb |
| | | el.datepicker.months.mar=Mar |
| | | el.datepicker.months.apr=Apr |
| | | el.datepicker.months.may=May |
| | | el.datepicker.months.jun=Jun |
| | | el.datepicker.months.jul=Jul |
| | | el.datepicker.months.aug=Aug |
| | | el.datepicker.months.sep=Sep |
| | | el.datepicker.months.oct=Oct |
| | | el.datepicker.months.nov=Nov |
| | | el.datepicker.months.dec=Dec |
| | | el.select.loading=Loading |
| | | el.select.noMatch=No matching data |
| | | el.select.noData=No data |
| | | el.select.placeholder=Please select |
| | | el.cascader.noMatch=No matching data |
| | | el.cascader.loading=Loading |
| | | el.cascader.placeholder=Please select |
| | | el.cascader.noData=No data |
| | | el.pagination.goto=Go to |
| | | el.pagination.pagesize=/page |
| | | el.pagination.total=Total {total} |
| | | el.pagination.pageClassifier= |
| | | el.messagebox.title=Prompt |
| | | el.messagebox.confirm=OK |
| | | el.messagebox.cancel=Cancel |
| | | el.messagebox.error=Invalid input |
| | | el.upload.deleteTip=Press delete to remove |
| | | el.upload.delete=Delete |
| | | el.upload.preview=Preview |
| | | el.upload.continue=Continue |
| | | legacy.regex.loopStatus=Zone {0} | Stations: {1} | Tasks: {2} | Load: {3} |
| | | legacy.regex.stationDeviceLink=Click to remove mapping: Station {0} -> Device {1} |
| | | deviceLogs.visualizationPrefix=Device Logs - |
| | | deviceLogs.downloadDialogTitle=Downloading file |
| | | llm.logsTitle=LLM Call Logs |
| | | llm.logDetailTitle=Log Details |
| | | llm.logDetailPrefix=Log Details - |
| | | el.table.emptyText=No data |
| | | el.table.confirmFilter=Columns |
| | | el.table.resetFilter=Reset |
| | | el.table.clearFilter=All |
| | | el.table.sumText=Sum |
| | | el.table.sort.ascending=Ascending |
| | | el.table.sort.descending=Descending |
| | | el.tree.emptyText=No data |
| | | el.transfer.noMatch=No matching data |
| | | el.transfer.noData=No data |
| | | el.transfer.titles.0=List 1 |
| | | el.transfer.titles.1=List 2 |
| | | el.transfer.filterPlaceholder=Enter keyword |
| | | el.transfer.noCheckedFormat=0 items |
| | | el.transfer.hasCheckedFormat={checked}/{total} checked |
| | | el.image.error=Load failed |
| | | el.pageHeader.title=Back |
| | | el.popconfirm.confirmButtonText=OK |
| | | el.popconfirm.cancelButtonText=Cancel |
| | | el.empty.description=No data |
| New file |
| | |
| | | 账号=账号 |
| | | 密码=密码 |
| | | 登录=登录 |
| | | 系统工具=系统工具 |
| | | 推荐操作=推荐操作 |
| | | 其他工具=其他工具 |
| | | 获取请求码=获取请求码 |
| | | 一键激活=一键激活 |
| | | 获取项目名称=获取项目名称 |
| | | 获取系统配置=获取系统配置 |
| | | 录入许可证=录入许可证 |
| | | 已复制到剪贴板=已复制到剪贴板 |
| | | 复制失败=复制失败 |
| | | 获取请求码失败=获取请求码失败 |
| | | 获取系统配置信息失败=获取系统配置信息失败 |
| | | 许可证内容不能为空=许可证内容不能为空 |
| | | 许可证更新成功=许可证更新成功 |
| | | 许可证更新失败=许可证更新失败 |
| | | 许可证录入失败=许可证录入失败 |
| | | 激活成功=激活成功 |
| | | 激活失败=激活失败 |
| | | 获取项目名称失败=获取项目名称失败 |
| | | 请输入账号=请输入账号 |
| | | 请输入密码=请输入密码 |
| | | 搜索菜单=搜索菜单 |
| | | 没有匹配菜单=没有匹配菜单 |
| | | 当前账号没有可用菜单=当前账号没有可用菜单 |
| | | 临时许可证有效期:=临时许可证有效期: |
| | | 仿真运行中=仿真运行中 |
| | | 仿真未运行=仿真未运行 |
| | | 基本资料=基本资料 |
| | | 退出登录=退出登录 |
| | | 关闭其他页签=关闭其他页签 |
| | | 返回控制中心=返回控制中心 |
| | | 许可证即将过期=许可证即将过期 |
| | | 知道了=知道了 |
| | | 控制中心=控制中心 |
| | | 实时监控=实时监控 |
| | | 账户中心=账户中心 |
| | | 管理员=管理员 |
| | | 正在加载页面...=正在加载页面... |
| | | AI助手=AI助手 |
| | | 确定要停止仿真模拟吗?=确定要停止仿真模拟吗? |
| | | 确定要启动仿真模拟吗?=确定要启动仿真模拟吗? |
| | | 仿真模拟已停止=仿真模拟已停止 |
| | | 仿真模拟已启动=仿真模拟已启动 |
| | | 操作失败=操作失败 |
| | | 菜单加载失败=菜单加载失败 |
| | | 菜单加载失败,请检查接口状态=菜单加载失败,请检查接口状态 |
| | | 工作页面=工作页面 |
| | | 业务页面=业务页面 |
| | | 编号=编号 |
| | | 起始时间 - 终止时间=起始时间 - 终止时间 |
| | | 请输入=请输入 |
| | | 请输入...=请输入... |
| | | 请选择数据=请选择数据 |
| | | 请选择要删除的数据=请选择要删除的数据 |
| | | 无数据=无数据 |
| | | 已存在=已存在 |
| | | 不可用=不可用 |
| | | 取消选择=取消选择 |
| | | 正常=正常 |
| | | 禁用=禁用 |
| | | 启用=启用 |
| | | 冻结=冻结 |
| | | 删除=删除 |
| | | 一级菜单=一级菜单 |
| | | 二级菜单=二级菜单 |
| | | 三级菜单=三级菜单 |
| | | 查询=查询 |
| | | 重置=重置 |
| | | 新增=新增 |
| | | 编辑=编辑 |
| | | 修改=修改 |
| | | 导出=导出 |
| | | 保存=保存 |
| | | 取消=取消 |
| | | 返回=返回 |
| | | 详情=详情 |
| | | 工作号=工作号 |
| | | WMS工作号=WMS工作号 |
| | | 源库位=源库位 |
| | | 目标库位=目标库位 |
| | | 堆垛机=堆垛机 |
| | | 双工位堆垛机=双工位堆垛机 |
| | | 区域编码=区域编码 |
| | | 未命名页面=未命名页面 |
| | | 未命名分组=未命名分组 |
| New file |
| | |
| | | lang.zh-CN=简体中文 |
| | | lang.en-US=英文 |
| | | app.title=浙江中扬 - 自动化立体仓库 - WCS |
| | | app.company=浙江中扬立库技术有限公司 |
| | | common.loadingPage=正在加载页面... |
| | | common.loadingTab=正在加载 “{0}” ... |
| | | common.refreshingTab=正在刷新 “{0}” ... |
| | | common.ok=好的 |
| | | common.prompt=提示 |
| | | common.language=语言 |
| | | common.profile=基本资料 |
| | | common.logout=退出登录 |
| | | common.closeOtherTabs=关闭其他页签 |
| | | common.backHome=返回控制中心 |
| | | common.aiAssistant=AI助手 |
| | | common.workPage=工作页面 |
| | | common.businessPage=业务页面 |
| | | login.title=WCS系统V3.0 |
| | | login.username=账号 |
| | | login.password=密码 |
| | | login.submit=登录 |
| | | login.tools.title=系统工具 |
| | | login.tools.recommended=推荐操作 |
| | | login.tools.recommendedDesc=优先使用“获取请求码”和“一键激活”完成许可证申请与激活。 |
| | | login.tools.others=其他工具 |
| | | login.tools.requestCode=获取请求码 |
| | | login.tools.activate=一键激活 |
| | | login.tools.projectName=获取项目名称 |
| | | login.tools.serverInfo=获取系统配置 |
| | | login.tools.uploadLicense=录入许可证 |
| | | login.dialog.copy=复制 |
| | | login.dialog.copied=已复制到剪贴板 |
| | | login.dialog.copyFailed=复制失败 |
| | | login.requestCode.title=获取请求码 |
| | | login.requestCode.label=请求码 |
| | | login.requestCode.tip=请求码中已包含项目名称,直接发给许可证服务端即可。 |
| | | login.serverInfo.title=获取系统配置 |
| | | login.serverInfo.label=系统配置信息 |
| | | login.serverInfo.tip=老项目仍可继续使用这份硬件信息 JSON 申请许可证。 |
| | | index.searchMenu=搜索菜单 |
| | | index.noMatchedMenu=没有匹配菜单 |
| | | index.noAvailableMenu=当前账号没有可用菜单 |
| | | index.licenseDays=临时许可证有效期:{0}天 |
| | | index.fakeRunning=仿真运行中 |
| | | index.fakeStopped=仿真未运行 |
| | | index.licenseExpiring=许可证即将过期 |
| | | index.homeTab=控制中心 |
| | | index.homeGroup=实时监控 |
| | | index.profileGroup=账户中心 |
| | | index.versionLoading=版本加载中... |
| | | index.licenseExpireAt=许可证将于 {0} 过期,剩余有效期:{1} 天。 |
| | | index.confirmStopFake=确定要停止仿真模拟吗? |
| | | index.confirmStartFake=确定要启动仿真模拟吗? |
| | | index.fakeStoppedSuccess=仿真模拟已停止 |
| | | index.fakeStartedSuccess=仿真模拟已启动 |
| | | index.operationFailed=操作失败 |
| | | index.menuLoadFailed=菜单加载失败 |
| | | index.menuLoadFailedDetail=菜单加载失败,请检查接口状态 |
| | | response.user.notFound=账号不存在 |
| | | response.user.disabled=账号已被禁用 |
| | | response.user.passwordMismatch=密码错误 |
| | | response.system.licenseExpired=许可证已失效 |
| | | response.common.systemError=系统异常,请稍后重试 |
| | | response.common.methodNotAllowed=请求方式不支持 |
| | | resource.index=控制中心 |
| | | resource.system=系统管理 |
| | | resource.set=系统配置 |
| | | resource.merchant=客户管理 |
| | | resource.develop=开发专用 |
| | | resource.stock=库存管理 |
| | | resource.logReport=日志报表 |
| | | resource.ioWork=入出库作业 |
| | | resource.workFlow=作业流程 |
| | | resource.base=基础资料 |
| | | resource.erp=ERP对接 |
| | | resource.sensor=感知设备 |
| | | resource.ai.llm_config=AI配置 |
| | | resource.notifyReport.notifyReport=通知上报 |
| | | resource.view=查看 |
| | | permission.function=指定功能 |
| | | el.colorpicker.confirm=确定 |
| | | el.colorpicker.clear=清空 |
| | | el.datepicker.now=此刻 |
| | | el.datepicker.today=今天 |
| | | el.datepicker.cancel=取消 |
| | | el.datepicker.clear=清空 |
| | | el.datepicker.confirm=确定 |
| | | el.datepicker.selectDate=选择日期 |
| | | el.datepicker.selectTime=选择时间 |
| | | el.datepicker.startDate=开始日期 |
| | | el.datepicker.startTime=开始时间 |
| | | el.datepicker.endDate=结束日期 |
| | | el.datepicker.endTime=结束时间 |
| | | el.datepicker.prevYear=前一年 |
| | | el.datepicker.nextYear=后一年 |
| | | el.datepicker.prevMonth=上个月 |
| | | el.datepicker.nextMonth=下个月 |
| | | el.datepicker.year=年 |
| | | el.datepicker.month1=1 月 |
| | | el.datepicker.month2=2 月 |
| | | el.datepicker.month3=3 月 |
| | | el.datepicker.month4=4 月 |
| | | el.datepicker.month5=5 月 |
| | | el.datepicker.month6=6 月 |
| | | el.datepicker.month7=7 月 |
| | | el.datepicker.month8=8 月 |
| | | el.datepicker.month9=9 月 |
| | | el.datepicker.month10=10 月 |
| | | el.datepicker.month11=11 月 |
| | | el.datepicker.month12=12 月 |
| | | el.datepicker.weeks.sun=日 |
| | | el.datepicker.weeks.mon=一 |
| | | el.datepicker.weeks.tue=二 |
| | | el.datepicker.weeks.wed=三 |
| | | el.datepicker.weeks.thu=四 |
| | | el.datepicker.weeks.fri=五 |
| | | el.datepicker.weeks.sat=六 |
| | | el.datepicker.months.jan=一月 |
| | | el.datepicker.months.feb=二月 |
| | | el.datepicker.months.mar=三月 |
| | | el.datepicker.months.apr=四月 |
| | | el.datepicker.months.may=五月 |
| | | el.datepicker.months.jun=六月 |
| | | el.datepicker.months.jul=七月 |
| | | el.datepicker.months.aug=八月 |
| | | el.datepicker.months.sep=九月 |
| | | el.datepicker.months.oct=十月 |
| | | el.datepicker.months.nov=十一月 |
| | | el.datepicker.months.dec=十二月 |
| | | el.select.loading=加载中 |
| | | el.select.noMatch=无匹配数据 |
| | | el.select.noData=无数据 |
| | | el.select.placeholder=请选择 |
| | | el.cascader.noMatch=无匹配数据 |
| | | el.cascader.loading=加载中 |
| | | el.cascader.placeholder=请选择 |
| | | el.cascader.noData=暂无数据 |
| | | el.pagination.goto=前往 |
| | | el.pagination.pagesize=条/页 |
| | | el.pagination.total=共 {total} 条 |
| | | el.pagination.pageClassifier=页 |
| | | el.messagebox.title=提示 |
| | | el.messagebox.confirm=确定 |
| | | el.messagebox.cancel=取消 |
| | | el.messagebox.error=输入的数据不合法 |
| | | el.upload.deleteTip=按 delete 键可删除 |
| | | el.upload.delete=删除 |
| | | el.upload.preview=查看图片 |
| | | el.upload.continue=继续上传 |
| | | legacy.regex.loopStatus=圈{0} | 站点: {1} | 任务: {2} | 承载: {3} |
| | | legacy.regex.stationDeviceLink=点击删除关联: 站点 {0} -> 设备 {1} |
| | | deviceLogs.visualizationPrefix=日志可视化 - |
| | | deviceLogs.downloadDialogTitle=文件下载中 |
| | | llm.logsTitle=LLM调用日志 |
| | | llm.logDetailTitle=日志详情 |
| | | llm.logDetailPrefix=日志详情 - |
| | | el.table.emptyText=暂无数据 |
| | | el.table.confirmFilter=筛选列 |
| | | el.table.resetFilter=重置 |
| | | el.table.clearFilter=全部 |
| | | el.table.sumText=合计 |
| | | el.table.sort.ascending=升序 |
| | | el.table.sort.descending=降序 |
| | | el.tree.emptyText=暂无数据 |
| | | el.transfer.noMatch=无匹配数据 |
| | | el.transfer.noData=无数据 |
| | | el.transfer.titles.0=列表 1 |
| | | el.transfer.titles.1=列表 2 |
| | | el.transfer.filterPlaceholder=请输入搜索内容 |
| | | el.transfer.noCheckedFormat=共 0 项 |
| | | el.transfer.hasCheckedFormat=已选 {checked}/{total} 项 |
| | | el.image.error=加载失败 |
| | | el.pageHeader.title=返回 |
| | | el.popconfirm.confirmButtonText=确定 |
| | | el.popconfirm.cancelButtonText=取消 |
| | | el.empty.description=暂无数据 |
| | |
| | | var baseUrl = "/wcs"; |
| | | |
| | | var WCS_I18N = (function (window, document) { |
| | | "use strict"; |
| | | |
| | | var STORAGE_KEY = "wcs_lang"; |
| | | var COOKIE_KEY = "wcs_lang"; |
| | | var I18N_PATH = "/i18n/messages"; |
| | | var TRANSLATABLE_RE = /[\u3400-\u9fff]/; |
| | | var state = { |
| | | ready: false, |
| | | loading: false, |
| | | locale: null, |
| | | defaultLocale: "zh-CN", |
| | | supportedLocales: ["zh-CN", "en-US"], |
| | | localeOptions: [], |
| | | messages: {}, |
| | | legacy: {}, |
| | | legacyEntries: [], |
| | | legacyRegexEntries: [], |
| | | builtinRegexEntries: [], |
| | | translateCache: {}, |
| | | translateCacheSize: 0, |
| | | callbacks: [], |
| | | observer: null, |
| | | bridgeAttempts: 0, |
| | | applying: false, |
| | | pendingNodes: [], |
| | | flushTimer: 0 |
| | | }; |
| | | |
| | | patchXmlHttpRequest(); |
| | | patchFetch(); |
| | | scheduleBridgeWrap(); |
| | | |
| | | if (document && document.documentElement) { |
| | | document.documentElement.lang = getLocale(); |
| | | } |
| | | |
| | | if (document.readyState === "loading") { |
| | | document.addEventListener("DOMContentLoaded", function () { |
| | | load(); |
| | | }); |
| | | } else { |
| | | load(); |
| | | } |
| | | |
| | | function normalizeLocale(locale) { |
| | | var value = (locale || "").replace(/_/g, "-").trim(); |
| | | if (!value) { |
| | | return state.defaultLocale; |
| | | } |
| | | if (/^en/i.test(value)) { |
| | | return "en-US"; |
| | | } |
| | | if (/^zh/i.test(value)) { |
| | | return "zh-CN"; |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | function getBasePath() { |
| | | return typeof baseUrl === "string" && baseUrl ? baseUrl : ""; |
| | | } |
| | | |
| | | function isExternalUrl(url) { |
| | | return /^(?:[a-z]+:)?\/\//i.test(url || ""); |
| | | } |
| | | |
| | | function appendNonceUrl(url) { |
| | | var value = (url || "").trim(); |
| | | if (!value || isExternalUrl(value) || value.charAt(0) === "<") { |
| | | return url; |
| | | } |
| | | if (/[?&]_wcsI18nNonce=/.test(value)) { |
| | | return value; |
| | | } |
| | | return value + (value.indexOf("?") === -1 ? "?" : "&") + "_wcsI18nNonce=" + new Date().getTime(); |
| | | } |
| | | |
| | | function getCookie(name) { |
| | | var pattern = new RegExp("(?:^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)"); |
| | | var matches = document.cookie.match(pattern); |
| | | return matches ? decodeURIComponent(matches[1]) : ""; |
| | | } |
| | | |
| | | function setCookie(name, value) { |
| | | var path = getBasePath() || "/"; |
| | | document.cookie = name + "=" + encodeURIComponent(value) + "; path=" + path + "; max-age=31536000"; |
| | | } |
| | | |
| | | function getStoredLocale() { |
| | | var locale = ""; |
| | | try { |
| | | locale = window.localStorage ? window.localStorage.getItem(STORAGE_KEY) : ""; |
| | | } catch (e) { |
| | | locale = ""; |
| | | } |
| | | if (!locale) { |
| | | locale = getCookie(COOKIE_KEY); |
| | | } |
| | | if (!locale && window.navigator) { |
| | | locale = window.navigator.language || (window.navigator.languages && window.navigator.languages[0]) || ""; |
| | | } |
| | | return normalizeLocale(locale || state.defaultLocale); |
| | | } |
| | | |
| | | function setStoredLocale(locale) { |
| | | var normalized = normalizeLocale(locale); |
| | | try { |
| | | if (window.localStorage) { |
| | | window.localStorage.setItem(STORAGE_KEY, normalized); |
| | | } |
| | | } catch (e) { |
| | | } |
| | | setCookie(COOKIE_KEY, normalized); |
| | | } |
| | | |
| | | function getLocale() { |
| | | if (!state.locale) { |
| | | state.locale = getStoredLocale(); |
| | | } |
| | | return state.locale; |
| | | } |
| | | |
| | | function setLocale(locale, reload) { |
| | | var normalized = normalizeLocale(locale); |
| | | state.locale = normalized; |
| | | setStoredLocale(normalized); |
| | | if (document && document.documentElement) { |
| | | document.documentElement.lang = normalized; |
| | | } |
| | | if (reload === false) { |
| | | state.ready = false; |
| | | load(); |
| | | return; |
| | | } |
| | | window.location.reload(); |
| | | } |
| | | |
| | | function onReady(callback) { |
| | | if (typeof callback !== "function") { |
| | | return; |
| | | } |
| | | if (state.ready) { |
| | | callback(api); |
| | | return; |
| | | } |
| | | state.callbacks.push(callback); |
| | | } |
| | | |
| | | function flushCallbacks() { |
| | | var queue = state.callbacks.slice(); |
| | | var i; |
| | | state.callbacks = []; |
| | | for (i = 0; i < queue.length; i++) { |
| | | try { |
| | | queue[i](api); |
| | | } catch (e) { |
| | | } |
| | | } |
| | | } |
| | | |
| | | function parseJson(text) { |
| | | try { |
| | | return JSON.parse(text); |
| | | } catch (e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | function request(url, success, failure) { |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open("GET", url, true); |
| | | xhr.setRequestHeader("X-Lang", getLocale()); |
| | | xhr.onreadystatechange = function () { |
| | | if (xhr.readyState !== 4) { |
| | | return; |
| | | } |
| | | if (xhr.status >= 200 && xhr.status < 300) { |
| | | success(parseJson(xhr.responseText) || {}); |
| | | } else if (typeof failure === "function") { |
| | | failure(xhr); |
| | | } |
| | | }; |
| | | xhr.send(null); |
| | | } |
| | | |
| | | function load(callback) { |
| | | if (typeof callback === "function") { |
| | | onReady(callback); |
| | | } |
| | | if (state.loading || state.ready) { |
| | | return; |
| | | } |
| | | state.loading = true; |
| | | request(getBasePath() + I18N_PATH + "?lang=" + encodeURIComponent(getLocale()) + "&_t=" + new Date().getTime(), function (res) { |
| | | var data = res && res.code === 200 ? (res.data || {}) : {}; |
| | | state.loading = false; |
| | | state.ready = true; |
| | | state.locale = normalizeLocale(data.locale || getLocale()); |
| | | state.defaultLocale = normalizeLocale(data.defaultLocale || state.defaultLocale); |
| | | state.supportedLocales = readLocaleTags(data.supportedLocales); |
| | | state.localeOptions = readLocaleOptions(data.supportedLocales); |
| | | state.messages = data.messages || {}; |
| | | state.legacy = data.legacy || {}; |
| | | state.legacyRegexEntries = readLegacyRegexEntries(state.legacy); |
| | | state.legacyEntries = sortedLegacyEntries(state.legacy); |
| | | state.builtinRegexEntries = buildBuiltinRegexEntries(); |
| | | resetTranslateCache(); |
| | | if (document && document.documentElement) { |
| | | document.documentElement.lang = state.locale; |
| | | } |
| | | wrapLibraries(); |
| | | observeDom(); |
| | | apply(document.body || document.documentElement); |
| | | if (document.title) { |
| | | document.title = translate(document.title); |
| | | } |
| | | flushCallbacks(); |
| | | }, function () { |
| | | state.loading = false; |
| | | state.ready = true; |
| | | state.locale = getLocale(); |
| | | state.builtinRegexEntries = buildBuiltinRegexEntries(); |
| | | resetTranslateCache(); |
| | | wrapLibraries(); |
| | | flushCallbacks(); |
| | | }); |
| | | } |
| | | |
| | | function readLocaleTags(options) { |
| | | var tags = []; |
| | | var i; |
| | | if (!options || !options.length) { |
| | | return state.supportedLocales.slice(); |
| | | } |
| | | for (i = 0; i < options.length; i++) { |
| | | if (typeof options[i] === "string") { |
| | | tags.push(normalizeLocale(options[i])); |
| | | } else if (options[i] && options[i].tag) { |
| | | tags.push(normalizeLocale(options[i].tag)); |
| | | } |
| | | } |
| | | return tags.length ? tags : state.supportedLocales.slice(); |
| | | } |
| | | |
| | | function readLocaleOptions(options) { |
| | | var list = []; |
| | | var i; |
| | | if (!options || !options.length) { |
| | | options = state.supportedLocales.slice(); |
| | | } |
| | | for (i = 0; i < options.length; i++) { |
| | | if (typeof options[i] === "string") { |
| | | list.push({ tag: normalizeLocale(options[i]), label: options[i] }); |
| | | } else if (options[i] && options[i].tag) { |
| | | list.push({ |
| | | tag: normalizeLocale(options[i].tag), |
| | | label: options[i].label || options[i].tag |
| | | }); |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | function sortedLegacyEntries(map) { |
| | | var entries = []; |
| | | var key; |
| | | for (key in map) { |
| | | if (map.hasOwnProperty(key) && key.indexOf("regex:") !== 0) { |
| | | entries.push([key, map[key]]); |
| | | } |
| | | } |
| | | entries.sort(function (left, right) { |
| | | return right[0].length - left[0].length; |
| | | }); |
| | | return entries; |
| | | } |
| | | |
| | | function readLegacyRegexEntries(map) { |
| | | var entries = []; |
| | | var key; |
| | | var source; |
| | | for (key in map) { |
| | | if (!map.hasOwnProperty(key) || key.indexOf("regex:") !== 0) { |
| | | continue; |
| | | } |
| | | source = key.substring(6); |
| | | if (!source) { |
| | | continue; |
| | | } |
| | | if (isBrokenRegexEntry(source, map[key])) { |
| | | continue; |
| | | } |
| | | try { |
| | | entries.push({ |
| | | source: source, |
| | | regex: new RegExp(source), |
| | | value: map[key] |
| | | }); |
| | | } catch (e) { |
| | | } |
| | | } |
| | | entries.sort(function (left, right) { |
| | | return right.source.length - left.source.length; |
| | | }); |
| | | return entries; |
| | | } |
| | | |
| | | function isBrokenRegexEntry(source, value) { |
| | | if (!source || !value) { |
| | | return false; |
| | | } |
| | | if (value.indexOf("$=") >= 0) { |
| | | return true; |
| | | } |
| | | return /\\s\*|\\d\+|\\\||\\:|\(\?:/.test(value); |
| | | } |
| | | |
| | | function format(template, params) { |
| | | var result = template || ""; |
| | | var index; |
| | | if (!params) { |
| | | return result; |
| | | } |
| | | if (Object.prototype.toString.call(params) === "[object Array]") { |
| | | for (index = 0; index < params.length; index++) { |
| | | result = result.replace(new RegExp("\\{" + index + "\\}", "g"), params[index]); |
| | | } |
| | | return result; |
| | | } |
| | | for (index in params) { |
| | | if (params.hasOwnProperty(index)) { |
| | | result = result.replace(new RegExp("\\{" + index + "\\}", "g"), params[index]); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | function resetTranslateCache() { |
| | | state.translateCache = {}; |
| | | state.translateCacheSize = 0; |
| | | } |
| | | |
| | | function rememberTranslateCache(text, translated) { |
| | | if (state.translateCacheSize > 4000) { |
| | | resetTranslateCache(); |
| | | } |
| | | if (!Object.prototype.hasOwnProperty.call(state.translateCache, text)) { |
| | | state.translateCacheSize += 1; |
| | | } |
| | | state.translateCache[text] = translated; |
| | | return translated; |
| | | } |
| | | |
| | | function canTranslateText(text) { |
| | | return !!(text && TRANSLATABLE_RE.test(text)); |
| | | } |
| | | |
| | | function buildBuiltinRegexEntries() { |
| | | return [ |
| | | { |
| | | regex: /^圈(\d+)\s*\|\s*站点:\s*(\d+)\s*\|\s*(?:任务|Task):\s*(\d+)\s*\|\s*承载:\s*([\d.]+%)$/, |
| | | key: "legacy.regex.loopStatus", |
| | | fallback: "Zone {0} | Stations: {1} | Tasks: {2} | Load: {3}" |
| | | }, |
| | | { |
| | | regex: /^点击(?:删除|Delete)关联:\s*站点\s*(\d+)\s*->\s*设备\s*(\d+)$/, |
| | | key: "legacy.regex.stationDeviceLink", |
| | | fallback: "Click to remove mapping: Station {0} -> Device {1}" |
| | | } |
| | | ]; |
| | | } |
| | | |
| | | function formatBuiltinRegex(entry, args) { |
| | | var value = t(entry.key, args); |
| | | if (value === entry.key && entry.fallback) { |
| | | return format(entry.fallback, args); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | function t(key, params) { |
| | | var value = state.messages[key]; |
| | | if (!value) { |
| | | return key; |
| | | } |
| | | return format(value, params); |
| | | } |
| | | |
| | | function preserveSpaces(original, translated) { |
| | | var match = original.match(/^(\s*)([\s\S]*?)(\s*)$/); |
| | | if (!match) { |
| | | return translated; |
| | | } |
| | | return match[1] + translated + match[3]; |
| | | } |
| | | |
| | | function exactLegacy(text) { |
| | | var trimmed = (text || "").trim(); |
| | | var translated = state.legacy[trimmed]; |
| | | if (!translated) { |
| | | return text; |
| | | } |
| | | return preserveSpaces(text, translated); |
| | | } |
| | | |
| | | function fragmentLegacy(text) { |
| | | var result = text; |
| | | var i; |
| | | var entry; |
| | | for (i = 0; i < state.legacyEntries.length; i++) { |
| | | entry = state.legacyEntries[i]; |
| | | if (!entry[0] || entry[0].length < 2 || entry[0] === entry[1]) { |
| | | continue; |
| | | } |
| | | result = result.split(entry[0]).join(entry[1]); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | function regexLegacy(text) { |
| | | var trimmed = (text || "").trim(); |
| | | var i; |
| | | var entry; |
| | | var translated; |
| | | for (i = 0; i < state.legacyRegexEntries.length; i++) { |
| | | entry = state.legacyRegexEntries[i]; |
| | | if (!entry || !entry.regex) { |
| | | continue; |
| | | } |
| | | if (trimmed) { |
| | | translated = trimmed.replace(entry.regex, entry.value); |
| | | if (translated !== trimmed) { |
| | | return preserveSpaces(text, translated); |
| | | } |
| | | } |
| | | translated = text.replace(entry.regex, entry.value); |
| | | if (translated !== text) { |
| | | return translated; |
| | | } |
| | | } |
| | | for (i = 0; i < state.builtinRegexEntries.length; i++) { |
| | | entry = state.builtinRegexEntries[i]; |
| | | if (!entry || !entry.regex) { |
| | | continue; |
| | | } |
| | | if (trimmed) { |
| | | translated = trimmed.replace(entry.regex, function () { |
| | | return formatBuiltinRegex(entry, Array.prototype.slice.call(arguments, 1, -2)); |
| | | }); |
| | | if (translated !== trimmed) { |
| | | return preserveSpaces(text, translated); |
| | | } |
| | | } |
| | | translated = text.replace(entry.regex, function () { |
| | | return formatBuiltinRegex(entry, Array.prototype.slice.call(arguments, 1, -2)); |
| | | }); |
| | | if (translated !== text) { |
| | | return translated; |
| | | } |
| | | } |
| | | return text; |
| | | } |
| | | |
| | | function translate(text) { |
| | | if (!text || !state.ready || state.locale === state.defaultLocale) { |
| | | return text; |
| | | } |
| | | if (Object.prototype.hasOwnProperty.call(state.translateCache, text)) { |
| | | return state.translateCache[text]; |
| | | } |
| | | if (!canTranslateText(text)) { |
| | | return rememberTranslateCache(text, text); |
| | | } |
| | | var exact = exactLegacy(text); |
| | | if (exact !== text) { |
| | | return rememberTranslateCache(text, exact); |
| | | } |
| | | var regex = regexLegacy(text); |
| | | if (regex !== text) { |
| | | return rememberTranslateCache(text, regex); |
| | | } |
| | | return rememberTranslateCache(text, fragmentLegacy(text)); |
| | | } |
| | | |
| | | function translateAttribute(element, attrName) { |
| | | var value = element.getAttribute(attrName); |
| | | var translated = translate(value); |
| | | if (translated && translated !== value) { |
| | | element.setAttribute(attrName, translated); |
| | | } |
| | | } |
| | | |
| | | function applyI18nKeys(element) { |
| | | var textKey = element.getAttribute("data-i18n-key"); |
| | | var placeholderKey = element.getAttribute("data-i18n-placeholder-key"); |
| | | var titleKey = element.getAttribute("data-i18n-title-key"); |
| | | var params = element.getAttribute("data-i18n-params"); |
| | | var parsedParams = params ? parseJson(params) : null; |
| | | if (textKey) { |
| | | if (/^(INPUT|TEXTAREA)$/i.test(element.tagName)) { |
| | | if (element.type === "button" || element.type === "submit" || element.type === "reset") { |
| | | var buttonText = t(textKey, parsedParams); |
| | | if (element.value !== buttonText) { |
| | | element.value = buttonText; |
| | | } |
| | | } |
| | | } else { |
| | | var textValue = t(textKey, parsedParams); |
| | | if (element.textContent !== textValue) { |
| | | element.textContent = textValue; |
| | | } |
| | | } |
| | | } |
| | | if (placeholderKey) { |
| | | var placeholderValue = t(placeholderKey, parsedParams); |
| | | if (element.getAttribute("placeholder") !== placeholderValue) { |
| | | element.setAttribute("placeholder", placeholderValue); |
| | | } |
| | | } |
| | | if (titleKey) { |
| | | var titleValue = t(titleKey, parsedParams); |
| | | if (element.getAttribute("title") !== titleValue) { |
| | | element.setAttribute("title", titleValue); |
| | | } |
| | | } |
| | | } |
| | | |
| | | function shouldSkipElement(element) { |
| | | if (!element || element.nodeType !== 1) { |
| | | return true; |
| | | } |
| | | return /^(SCRIPT|STYLE|TEXTAREA|CODE|PRE)$/i.test(element.tagName); |
| | | } |
| | | |
| | | function traverse(node) { |
| | | var child; |
| | | if (!node) { |
| | | return; |
| | | } |
| | | if (node.nodeType === 3) { |
| | | var translatedText = translate(node.nodeValue); |
| | | if (translatedText !== node.nodeValue) { |
| | | node.nodeValue = translatedText; |
| | | } |
| | | return; |
| | | } |
| | | if (node.nodeType !== 1 || shouldSkipElement(node)) { |
| | | return; |
| | | } |
| | | applyI18nKeys(node); |
| | | translateAttribute(node, "placeholder"); |
| | | translateAttribute(node, "title"); |
| | | translateAttribute(node, "aria-label"); |
| | | translateAttribute(node, "alt"); |
| | | if (/^(INPUT)$/i.test(node.tagName)) { |
| | | if (/^(button|submit|reset)$/i.test(node.type || "")) { |
| | | if (node.value) { |
| | | var translatedValue = translate(node.value); |
| | | if (translatedValue !== node.value) { |
| | | node.value = translatedValue; |
| | | } |
| | | } |
| | | } else if ((node.readOnly || node.disabled || node.getAttribute("readonly") !== null) && node.value) { |
| | | var translatedReadonlyValue = translate(node.value); |
| | | if (translatedReadonlyValue !== node.value) { |
| | | node.value = translatedReadonlyValue; |
| | | } |
| | | } |
| | | } |
| | | child = node.firstChild; |
| | | while (child) { |
| | | traverse(child); |
| | | child = child.nextSibling; |
| | | } |
| | | } |
| | | |
| | | function apply(root) { |
| | | if (!root || !state.ready) { |
| | | return; |
| | | } |
| | | state.applying = true; |
| | | try { |
| | | traverse(root); |
| | | } finally { |
| | | state.applying = false; |
| | | } |
| | | } |
| | | |
| | | function scheduleFlush() { |
| | | if (state.flushTimer) { |
| | | return; |
| | | } |
| | | var scheduler = window.requestAnimationFrame || function (callback) { |
| | | return window.setTimeout(callback, 16); |
| | | }; |
| | | state.flushTimer = scheduler(flushPendingNodes); |
| | | } |
| | | |
| | | function enqueueNode(node) { |
| | | if (!node || node.__wcsI18nQueued) { |
| | | return; |
| | | } |
| | | node.__wcsI18nQueued = true; |
| | | state.pendingNodes.push(node); |
| | | scheduleFlush(); |
| | | } |
| | | |
| | | function flushPendingNodes() { |
| | | var queue; |
| | | var i; |
| | | state.flushTimer = 0; |
| | | if (!state.pendingNodes.length) { |
| | | return; |
| | | } |
| | | if (state.applying) { |
| | | scheduleFlush(); |
| | | return; |
| | | } |
| | | queue = state.pendingNodes.slice(); |
| | | state.pendingNodes = []; |
| | | state.applying = true; |
| | | try { |
| | | for (i = 0; i < queue.length; i++) { |
| | | if (!queue[i]) { |
| | | continue; |
| | | } |
| | | queue[i].__wcsI18nQueued = false; |
| | | if (queue[i].nodeType === 3) { |
| | | var translatedText = translate(queue[i].nodeValue); |
| | | if (translatedText !== queue[i].nodeValue) { |
| | | queue[i].nodeValue = translatedText; |
| | | } |
| | | } else if (queue[i].nodeType === 1) { |
| | | traverse(queue[i]); |
| | | } |
| | | } |
| | | } finally { |
| | | state.applying = false; |
| | | } |
| | | if (state.pendingNodes.length) { |
| | | scheduleFlush(); |
| | | } |
| | | } |
| | | |
| | | function observeDom() { |
| | | if (state.observer || !window.MutationObserver || !document || !document.documentElement) { |
| | | return; |
| | | } |
| | | state.observer = new MutationObserver(function (mutations) { |
| | | var i; |
| | | if (state.applying) { |
| | | return; |
| | | } |
| | | for (i = 0; i < mutations.length; i++) { |
| | | if (mutations[i].type === "childList") { |
| | | for (var j = 0; j < mutations[i].addedNodes.length; j++) { |
| | | enqueueNode(mutations[i].addedNodes[j]); |
| | | } |
| | | } else if (mutations[i].type === "characterData" && mutations[i].target) { |
| | | enqueueNode(mutations[i].target); |
| | | } else if (mutations[i].type === "attributes" && mutations[i].target) { |
| | | enqueueNode(mutations[i].target); |
| | | } |
| | | } |
| | | }); |
| | | state.observer.observe(document.documentElement, { |
| | | childList: true, |
| | | subtree: true, |
| | | characterData: true, |
| | | attributes: true, |
| | | attributeFilter: ["placeholder", "title", "aria-label", "alt"] |
| | | }); |
| | | } |
| | | |
| | | function wrapLayer() { |
| | | if (!window.layer || window.layer.__wcsI18nWrapped) { |
| | | return; |
| | | } |
| | | var layer = window.layer; |
| | | var rawMsg = layer.msg; |
| | | var rawAlert = layer.alert; |
| | | var rawConfirm = layer.confirm; |
| | | var rawTips = layer.tips; |
| | | var rawOpen = layer.open; |
| | | |
| | | if (typeof rawMsg === "function") { |
| | | layer.msg = function (content, options, end) { |
| | | return rawMsg.call(layer, translate(content), options, end); |
| | | }; |
| | | } |
| | | if (typeof rawAlert === "function") { |
| | | layer.alert = function (content, options, yes) { |
| | | if (options && typeof options === "object" && typeof options.title === "string") { |
| | | options.title = translate(options.title); |
| | | } |
| | | return rawAlert.call(layer, translate(content), options, yes); |
| | | }; |
| | | } |
| | | if (typeof rawConfirm === "function") { |
| | | layer.confirm = function (content, options, yes, cancel) { |
| | | if (typeof options === "string") { |
| | | options = translate(options); |
| | | } else if (options && typeof options === "object") { |
| | | if (typeof options.title === "string") { |
| | | options.title = translate(options.title); |
| | | } else if (!options.title) { |
| | | options.title = translate("信息"); |
| | | } |
| | | if (Object.prototype.toString.call(options.btn) === "[object Array]") { |
| | | for (var i = 0; i < options.btn.length; i++) { |
| | | options.btn[i] = translate(options.btn[i]); |
| | | } |
| | | } |
| | | } |
| | | return rawConfirm.call(layer, translate(content), options, yes, cancel); |
| | | }; |
| | | } |
| | | if (typeof rawTips === "function") { |
| | | layer.tips = function (content, follow, options) { |
| | | return rawTips.call(layer, translate(content), follow, options); |
| | | }; |
| | | } |
| | | if (typeof rawOpen === "function") { |
| | | layer.open = function (options) { |
| | | if (options && typeof options === "object") { |
| | | if (typeof options.title === "string") { |
| | | options.title = translate(options.title); |
| | | } else if (Object.prototype.toString.call(options.title) === "[object Array]" && typeof options.title[0] === "string") { |
| | | options.title[0] = translate(options.title[0]); |
| | | } |
| | | if (typeof options.content === "string") { |
| | | if (options.type === 2) { |
| | | options.content = appendNonceUrl(options.content); |
| | | } else { |
| | | options.content = translate(options.content); |
| | | } |
| | | } |
| | | if (Object.prototype.toString.call(options.btn) === "[object Array]") { |
| | | for (var i = 0; i < options.btn.length; i++) { |
| | | options.btn[i] = translate(options.btn[i]); |
| | | } |
| | | } |
| | | } |
| | | return rawOpen.call(layer, options); |
| | | }; |
| | | } |
| | | window.layer.__wcsI18nWrapped = true; |
| | | } |
| | | |
| | | function wrapElement() { |
| | | if (!window.ELEMENT || window.ELEMENT.__wcsI18nWrapped) { |
| | | return; |
| | | } |
| | | var element = window.ELEMENT; |
| | | var methods = ["success", "warning", "info", "error"]; |
| | | var i; |
| | | if (typeof element.i18n === "function") { |
| | | element.i18n(function (key, value) { |
| | | return t(key, value); |
| | | }); |
| | | } |
| | | if (typeof element.Message === "function") { |
| | | var rawMessage = element.Message; |
| | | element.Message = function (options) { |
| | | if (typeof options === "string") { |
| | | return rawMessage.call(element, translate(options)); |
| | | } |
| | | if (options && typeof options === "object" && typeof options.message === "string") { |
| | | options.message = translate(options.message); |
| | | } |
| | | return rawMessage.call(element, options); |
| | | }; |
| | | for (i = 0; i < methods.length; i++) { |
| | | if (typeof rawMessage[methods[i]] === "function") { |
| | | (function (methodName) { |
| | | element.Message[methodName] = function (options) { |
| | | if (typeof options === "string") { |
| | | return rawMessage[methodName].call(rawMessage, translate(options)); |
| | | } |
| | | if (options && typeof options === "object" && typeof options.message === "string") { |
| | | options.message = translate(options.message); |
| | | } |
| | | return rawMessage[methodName].call(rawMessage, options); |
| | | }; |
| | | })(methods[i]); |
| | | } |
| | | } |
| | | } |
| | | if (element.MessageBox) { |
| | | wrapMessageBoxMethod(element.MessageBox, "alert"); |
| | | wrapMessageBoxMethod(element.MessageBox, "confirm"); |
| | | wrapMessageBoxMethod(element.MessageBox, "prompt"); |
| | | } |
| | | window.ELEMENT.__wcsI18nWrapped = true; |
| | | } |
| | | |
| | | function wrapMessageBoxMethod(box, methodName) { |
| | | if (typeof box[methodName] !== "function") { |
| | | return; |
| | | } |
| | | var raw = box[methodName]; |
| | | box[methodName] = function (message, title, options) { |
| | | if (typeof message === "string") { |
| | | message = translate(message); |
| | | } |
| | | if (typeof title === "string") { |
| | | title = translate(title); |
| | | } else if (title && typeof title === "object" && typeof title.message === "string") { |
| | | title.message = translate(title.message); |
| | | } |
| | | if (options && typeof options === "object") { |
| | | if (typeof options.confirmButtonText === "string") { |
| | | options.confirmButtonText = translate(options.confirmButtonText); |
| | | } |
| | | if (typeof options.cancelButtonText === "string") { |
| | | options.cancelButtonText = translate(options.cancelButtonText); |
| | | } |
| | | if (typeof options.title === "string") { |
| | | options.title = translate(options.title); |
| | | } |
| | | } |
| | | return raw.call(box, message, title, options); |
| | | }; |
| | | } |
| | | |
| | | function wrapLibraries() { |
| | | wrapLayer(); |
| | | wrapElement(); |
| | | } |
| | | |
| | | function scheduleBridgeWrap() { |
| | | var timer = window.setInterval(function () { |
| | | state.bridgeAttempts += 1; |
| | | wrapLibraries(); |
| | | if ((window.layer || !window.ELEMENT) && state.bridgeAttempts > 20) { |
| | | window.clearInterval(timer); |
| | | } |
| | | }, 300); |
| | | } |
| | | |
| | | function patchXmlHttpRequest() { |
| | | if (!window.XMLHttpRequest || window.XMLHttpRequest.__wcsI18nPatched) { |
| | | return; |
| | | } |
| | | var rawOpen = window.XMLHttpRequest.prototype.open; |
| | | var rawSend = window.XMLHttpRequest.prototype.send; |
| | | var rawSetHeader = window.XMLHttpRequest.prototype.setRequestHeader; |
| | | |
| | | window.XMLHttpRequest.prototype.open = function (method, url) { |
| | | this.__wcsI18nUrl = url; |
| | | this.__wcsI18nHeaderSet = false; |
| | | return rawOpen.apply(this, arguments); |
| | | }; |
| | | window.XMLHttpRequest.prototype.setRequestHeader = function (name, value) { |
| | | if (String(name).toLowerCase() === "x-lang") { |
| | | this.__wcsI18nHeaderSet = true; |
| | | } |
| | | return rawSetHeader.apply(this, arguments); |
| | | }; |
| | | window.XMLHttpRequest.prototype.send = function () { |
| | | try { |
| | | if (!this.__wcsI18nHeaderSet && shouldAttachLangHeader(this.__wcsI18nUrl)) { |
| | | rawSetHeader.call(this, "X-Lang", getLocale()); |
| | | this.__wcsI18nHeaderSet = true; |
| | | } |
| | | } catch (e) { |
| | | } |
| | | return rawSend.apply(this, arguments); |
| | | }; |
| | | window.XMLHttpRequest.__wcsI18nPatched = true; |
| | | } |
| | | |
| | | function patchFetch() { |
| | | if (!window.fetch || window.fetch.__wcsI18nPatched) { |
| | | return; |
| | | } |
| | | var rawFetch = window.fetch; |
| | | window.fetch = function (input, init) { |
| | | var requestUrl = typeof input === "string" ? input : (input && input.url); |
| | | init = init || {}; |
| | | if (shouldAttachLangHeader(requestUrl)) { |
| | | init.headers = init.headers || {}; |
| | | if (typeof Headers !== "undefined" && init.headers instanceof Headers) { |
| | | if (!init.headers.has("X-Lang")) { |
| | | init.headers.set("X-Lang", getLocale()); |
| | | } |
| | | } else if (!init.headers["X-Lang"] && !init.headers["x-lang"]) { |
| | | init.headers["X-Lang"] = getLocale(); |
| | | } |
| | | } |
| | | return rawFetch.call(window, input, init); |
| | | }; |
| | | window.fetch.__wcsI18nPatched = true; |
| | | } |
| | | |
| | | function shouldAttachLangHeader(url) { |
| | | if (!url) { |
| | | return true; |
| | | } |
| | | if (typeof url !== "string") { |
| | | return true; |
| | | } |
| | | return url.indexOf("http://") !== 0 && url.indexOf("https://") !== 0 || url.indexOf(window.location.origin) === 0; |
| | | } |
| | | |
| | | var api = { |
| | | load: load, |
| | | onReady: onReady, |
| | | t: t, |
| | | tl: translate, |
| | | apply: apply, |
| | | getLocale: getLocale, |
| | | setLocale: setLocale, |
| | | getLocaleOptions: function () { |
| | | return state.localeOptions.slice(); |
| | | }, |
| | | getSupportedLocales: function () { |
| | | return state.supportedLocales.slice(); |
| | | }, |
| | | getDefaultLocale: function () { |
| | | return state.defaultLocale; |
| | | }, |
| | | addNonceUrl: appendNonceUrl, |
| | | isReady: function () { |
| | | return state.ready; |
| | | } |
| | | }; |
| | | |
| | | return api; |
| | | })(window, document); |
| | | |
| | | // 赋值 |
| | | function setVal(el, val) { |
| | | if (el.text() !== val){ |
| | |
| | | return this.deviceList; |
| | | }, |
| | | visualizationTitle() { |
| | | return `日志可视化 - ${this.visDeviceType} ${this.visDeviceNo} (${this.searchForm.day})`; |
| | | return this.i18n('deviceLogs.visualizationPrefix', '日志可视化 - ') + this.visDeviceType + ' ' + this.visDeviceNo + ' (' + this.searchForm.day + ')'; |
| | | }, |
| | | downloadDialogTitle() { |
| | | return this.i18n('deviceLogs.downloadDialogTitle', '文件下载中'); |
| | | }, |
| | | maxSliderValue() { |
| | | return Math.max(0, this.endTime - this.startTime); |
| | |
| | | this.loadDeviceEnums(); |
| | | this.loadDateTree(); |
| | | }, |
| | | mounted() { |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') { |
| | | let that = this; |
| | | window.WCS_I18N.onReady(function () { |
| | | that.$forceUpdate(); |
| | | }); |
| | | } |
| | | }, |
| | | methods: { |
| | | i18n(key, fallback, params) { |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.t === 'function') { |
| | | var translated = window.WCS_I18N.t(key, params); |
| | | if (translated && translated !== key) { |
| | | return translated; |
| | | } |
| | | } |
| | | return fallback || key; |
| | | }, |
| | | // --- Initialization --- |
| | | loadDeviceEnums() { |
| | | let that = this; |
| | |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | |
| | | <div class="route-card" :class="routeCardClass(route)" v-for="(route, idx) in routes" :key="route.id ? ('route_' + route.id) : ('new_' + idx)"> |
| | | <div class="route-head"> |
| | | <div class="route-title"> |
| | | <el-input v-model="route.name" size="mini" placeholder="路由名称"></el-input> |
| | | <el-input |
| | | :value="displayRouteName(route)" |
| | | size="mini" |
| | | placeholder="路由名称" |
| | | @input="handleRouteNameInput(route, $event)"></el-input> |
| | | <div class="route-id-line">#{{ route.id || 'new' }} · 优先级 {{ route.priority || 0 }}</div> |
| | | </div> |
| | | <div class="route-state"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <el-dialog title="LLM调用日志" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false"> |
| | | <el-dialog :title="i18n('llm.logsTitle', 'LLM调用日志')" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false"> |
| | | <div class="log-toolbar"> |
| | | <el-select v-model="logQuery.scene" size="mini" clearable placeholder="场景" style="width:180px;"> |
| | | <el-option label="chat" value="chat"></el-option> |
| | |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <el-dialog :title="logDetailTitle || '日志详情'" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body> |
| | | <el-dialog :title="logDetailTitle || i18n('llm.logDetailTitle', '日志详情')" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body> |
| | | <div class="log-detail-body">{{ logDetailText || '-' }}</div> |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button size="mini" @click="copyText(logDetailText)">复制全文</el-button> |
| | |
| | | } |
| | | }, |
| | | methods: { |
| | | i18n: function(key, fallback, params) { |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.t === 'function') { |
| | | var translated = window.WCS_I18N.t(key, params); |
| | | if (translated && translated !== key) { |
| | | return translated; |
| | | } |
| | | } |
| | | return fallback || key; |
| | | }, |
| | | translateLegacyText: function(text) { |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.tl === 'function') { |
| | | return window.WCS_I18N.tl(text); |
| | | } |
| | | return text; |
| | | }, |
| | | displayRouteName: function(route) { |
| | | var value = route && route.name ? String(route.name) : ''; |
| | | return this.translateLegacyText(value); |
| | | }, |
| | | handleRouteNameInput: function(route, value) { |
| | | if (!route) { |
| | | return; |
| | | } |
| | | route.name = value; |
| | | }, |
| | | formatDateTime: function(input) { |
| | | if (!input) return '-'; |
| | | var d = input instanceof Date ? input : new Date(input); |
| | |
| | | + '错误: ' + (row.errorMessage || '-') + '\n\n' |
| | | + '请求:\n' + (row.requestContent || '-') + '\n\n' |
| | | + '响应:\n' + (row.responseContent || '-'); |
| | | this.logDetailTitle = '日志详情 - ' + (row.traceId || row.id || ''); |
| | | this.logDetailTitle = this.i18n('llm.logDetailPrefix', '日志详情 - ') + (row.traceId || row.id || ''); |
| | | this.logDetailText = text; |
| | | this.logDetailVisible = true; |
| | | }, |
| | |
| | | } |
| | | }, |
| | | mounted: function() { |
| | | var self = this; |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') { |
| | | window.WCS_I18N.onReady(function() { |
| | | self.$forceUpdate(); |
| | | }); |
| | | } |
| | | this.loadRoutes(); |
| | | } |
| | | }); |
| | |
| | | </el-dialog> |
| | | |
| | | <!-- Download Progress Dialog --> |
| | | <el-dialog title="文件下载中" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false"> |
| | | <el-dialog :title="downloadDialogTitle" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false"> |
| | | <div style="padding: 10px;"> |
| | | <div style="margin-bottom: 5px; font-size: 14px;">压缩生成进度</div> |
| | | <el-progress :percentage="buildProgress" :text-inside="true" :stroke-width="18"></el-progress> |
| | |
| | | <script src="../../components/WatchRgvCard.js"></script> |
| | | <script src="../../components/WatchDualCrnCard.js"></script> |
| | | <script src="../../components/DevpCard.js"></script> |
| | | <script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js?v=20260309_i18n_pagefix1" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .lang-select { |
| | | width: 160px; |
| | | } |
| | | |
| | | .header-right .el-tag { |
| | | border-radius: 999px; |
| | | } |
| | |
| | | size="small" |
| | | clearable |
| | | prefix-icon="el-icon-search" |
| | | placeholder="搜索菜单"> |
| | | :placeholder="t('index.searchMenu')"> |
| | | </el-input> |
| | | </div> |
| | | |
| | |
| | | <div class="aside-empty" v-if="filteredMenus.length === 0"> |
| | | <el-empty |
| | | :image-size="80" |
| | | :description="menuKeyword ? '没有匹配菜单' : '当前账号没有可用菜单'"> |
| | | :description="menuKeyword ? t('index.noMatchedMenu') : t('index.noAvailableMenu')"> |
| | | </el-empty> |
| | | </div> |
| | | </template> |
| | | </el-scrollbar> |
| | | |
| | | <div class="aside-footer" v-show="!isCollapse"> |
| | | <div class="aside-footer-copy">© 2026 浙江中扬立库技术有限公司</div> |
| | | <div class="aside-footer-copy">© 2026 {{ t('app.company') }}</div> |
| | | <div class="aside-footer-version"> |
| | | <span class="aside-footer-version-text">{{ versionText }}</span> |
| | | <el-tag |
| | |
| | | size="mini" |
| | | :type="licenseTagType" |
| | | effect="dark"> |
| | | 临时许可证有效期:{{ licenseDays }}天 |
| | | {{ t('index.licenseDays', [licenseDays]) }} |
| | | </el-tag> |
| | | <el-tag |
| | | v-if="fakeVisible" |
| | |
| | | :type="fakeRunning ? 'danger' : 'info'" |
| | | effect="dark" |
| | | @click.native="toggleFakeSystem"> |
| | | {{ fakeRunning ? '仿真运行中' : '仿真未运行' }} |
| | | {{ fakeRunning ? t('index.fakeRunning') : t('index.fakeStopped') }} |
| | | </el-tag> |
| | | <el-select |
| | | v-model="currentLocale" |
| | | size="mini" |
| | | class="lang-select" |
| | | @change="handleLocaleChange"> |
| | | <el-option |
| | | v-for="item in localeOptions" |
| | | :key="item.tag" |
| | | :label="item.label" |
| | | :value="item.tag"> |
| | | </el-option> |
| | | </el-select> |
| | | <el-button circle size="mini" icon="el-icon-refresh" @click="refreshActiveTab"></el-button> |
| | | <el-button circle size="mini" icon="el-icon-full-screen" @click="toggleFullScreen"></el-button> |
| | | |
| | |
| | | <i class="el-icon-arrow-down"></i> |
| | | </span> |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <el-dropdown-item command="profile">基本资料</el-dropdown-item> |
| | | <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item> |
| | | <el-dropdown-item command="profile">{{ t('common.profile') }}</el-dropdown-item> |
| | | <el-dropdown-item command="logout" divided>{{ t('common.logout') }}</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </el-dropdown> |
| | | </div> |
| | |
| | | </el-tabs> |
| | | |
| | | <div class="tabs-tools"> |
| | | <el-tooltip content="关闭其他页签" placement="top"> |
| | | <el-tooltip :content="t('common.closeOtherTabs')" placement="top"> |
| | | <el-button size="mini" icon="el-icon-close" @click="closeOtherTabs"></el-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="返回控制中心" placement="top"> |
| | | <el-tooltip :content="t('common.backHome')" placement="top"> |
| | | <el-button size="mini" type="primary" icon="el-icon-house" @click="openHomeTab"></el-button> |
| | | </el-tooltip> |
| | | </div> |
| | |
| | | v-for="tab in tabs" |
| | | :key="'frame-' + tab.name" |
| | | class="page-frame" |
| | | :data-tab-name="tab.name" |
| | | v-show="activeTab === tab.name" |
| | | :src="tab.currentSrc" |
| | | @load="handleFrameLoad(tab.name)"> |
| | |
| | | </el-container> |
| | | |
| | | <el-dialog |
| | | title="许可证即将过期" |
| | | :title="t('index.licenseExpiring')" |
| | | :visible.sync="licenseDialogVisible" |
| | | width="420px" |
| | | :close-on-click-modal="false"> |
| | | <div class="license-dialog-text">{{ licenseDialogText }}</div> |
| | | <span slot="footer"> |
| | | <el-button type="primary" @click="licenseDialogVisible = false">知道了</el-button> |
| | | <el-button type="primary" @click="licenseDialogVisible = false">{{ t('common.ok') }}</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | |
| | | }; |
| | | var TAB_STORAGE_KEY = "wcs-element-home-tabs"; |
| | | var USER_STORAGE_KEY = "username"; |
| | | var INDEX_I18N_FALLBACKS = { |
| | | "app.title": "浙江中扬 - 自动化立体仓库 - WCS", |
| | | "app.company": "浙江中扬立库技术有限公司", |
| | | "common.loadingPage": "正在加载页面...", |
| | | "common.loadingTab": "正在加载 “{0}” ...", |
| | | "common.refreshingTab": "正在刷新 “{0}” ...", |
| | | "common.ok": "知道了", |
| | | "common.prompt": "提示", |
| | | "common.profile": "基本资料", |
| | | "common.logout": "退出登录", |
| | | "common.closeOtherTabs": "关闭其他页签", |
| | | "common.backHome": "返回控制中心", |
| | | "common.aiAssistant": "AI助手", |
| | | "common.workPage": "工作页面", |
| | | "common.businessPage": "业务页面", |
| | | "index.searchMenu": "搜索菜单", |
| | | "index.noMatchedMenu": "没有匹配菜单", |
| | | "index.noAvailableMenu": "当前账号没有可用菜单", |
| | | "index.licenseDays": "临时许可证有效期:{0}天", |
| | | "index.fakeRunning": "仿真运行中", |
| | | "index.fakeStopped": "仿真未运行", |
| | | "index.licenseExpiring": "许可证即将过期", |
| | | "index.homeTab": "控制中心", |
| | | "index.homeGroup": "实时监控", |
| | | "index.profileGroup": "账户中心", |
| | | "index.versionLoading": "Version loading...", |
| | | "index.licenseExpireAt": "许可证将于 {0} 过期,剩余有效期:{1} 天。", |
| | | "index.confirmStopFake": "确定要停止仿真模拟吗?", |
| | | "index.confirmStartFake": "确定要启动仿真模拟吗?", |
| | | "index.fakeStoppedSuccess": "仿真模拟已停止", |
| | | "index.fakeStartedSuccess": "仿真模拟已启动", |
| | | "index.operationFailed": "操作失败", |
| | | "index.menuLoadFailed": "菜单加载失败", |
| | | "index.menuLoadFailedDetail": "菜单加载失败,请检查接口状态" |
| | | }; |
| | | |
| | | new Vue({ |
| | | el: "#app", |
| | |
| | | isCollapse: false, |
| | | menuLoading: true, |
| | | pageLoading: true, |
| | | loadingText: "正在加载页面...", |
| | | loadingText: window.WCS_I18N ? window.WCS_I18N.tl("正在加载页面...") : "正在加载页面...", |
| | | menuKeyword: "", |
| | | menus: [], |
| | | defaultOpeneds: [], |
| | |
| | | licenseDays: null, |
| | | licenseDialogVisible: false, |
| | | licenseDialogText: "", |
| | | currentLocale: window.WCS_I18N ? window.WCS_I18N.getLocale() : "zh-CN", |
| | | localeOptions: [], |
| | | fakeVisible: false, |
| | | fakeRunning: false, |
| | | fakeStatusInterval: null, |
| | | menuSyncVersion: 0, |
| | | menuSyncTimer: null, |
| | | userName: localStorage.getItem(USER_STORAGE_KEY) || "管理员", |
| | | userName: localStorage.getItem(USER_STORAGE_KEY) || (window.WCS_I18N ? window.WCS_I18N.tl("管理员") : "管理员"), |
| | | aiLayerIndex: null, |
| | | aiTipIndex: null |
| | | }; |
| | |
| | | return result; |
| | | }, |
| | | activeTabMeta: function () { |
| | | return this.getTabByName(this.activeTab) || this.createTab(HOME_TAB_CONFIG); |
| | | return this.getTabByName(this.activeTab) || this.createTab(this.resolveHomeConfig()); |
| | | }, |
| | | activeTabTitle: function () { |
| | | return this.activeTabMeta.title; |
| | |
| | | }, |
| | | versionText: function () { |
| | | if (!this.version) { |
| | | return "Version loading..."; |
| | | return this.t("index.versionLoading"); |
| | | } |
| | | return "Version " + this.version; |
| | | }, |
| | |
| | | return "info"; |
| | | }, |
| | | userShortName: function () { |
| | | return (this.userName || "管理员").substring(0, 1); |
| | | return (this.userName || this.tl("管理员")).substring(0, 1); |
| | | } |
| | | }, |
| | | watch: { |
| | | activeTab: function () { |
| | | var tab = this.getTabByName(this.activeTab); |
| | | this.syncMenuStateByUrl(tab ? tab.url : HOME_TAB_CONFIG.url); |
| | | this.syncMenuStateByUrl(tab ? tab.url : this.resolveHomeConfig().url); |
| | | this.pageLoading = !!(tab && !tab.loaded); |
| | | if (this.pageLoading) { |
| | | this.loadingText = "正在加载 “" + tab.title + "” ..."; |
| | | this.loadingText = this.t("common.loadingTab", [tab.title]); |
| | | } |
| | | document.title = (tab ? tab.title : HOME_TAB_CONFIG.title) + " - 浙江中扬 - 自动化立体仓库 - WCS"; |
| | | this.updateDocumentTitle(tab ? tab.title : this.resolveHomeConfig().title); |
| | | this.persistTabs(); |
| | | } |
| | | }, |
| | |
| | | } |
| | | |
| | | this.restoreTabs(); |
| | | this.bindI18n(); |
| | | this.installCompatBridge(); |
| | | this.loadSystemVersion(); |
| | | this.loadMenu(); |
| | |
| | | }, |
| | | mounted: function () { |
| | | $("#ai-assistant-btn").html(getAiIconHtml(60, 60)); |
| | | document.title = this.activeTabTitle + " - 浙江中扬 - 自动化立体仓库 - WCS"; |
| | | this.updateDocumentTitle(this.activeTabTitle); |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.fakeStatusInterval) { |
| | |
| | | } |
| | | }, |
| | | methods: { |
| | | t: function (key, params) { |
| | | var value = window.WCS_I18N ? window.WCS_I18N.t(key, params) : key; |
| | | if (value !== key) { |
| | | return value; |
| | | } |
| | | value = INDEX_I18N_FALLBACKS[key] || key; |
| | | if (!params) { |
| | | return value; |
| | | } |
| | | if (Object.prototype.toString.call(params) === "[object Array]") { |
| | | for (var i = 0; i < params.length; i++) { |
| | | value = value.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]); |
| | | } |
| | | return value; |
| | | } |
| | | return value; |
| | | }, |
| | | tl: function (text) { |
| | | return window.WCS_I18N ? window.WCS_I18N.tl(text) : text; |
| | | }, |
| | | bindI18n: function () { |
| | | var that = this; |
| | | if (!window.WCS_I18N) { |
| | | return; |
| | | } |
| | | window.WCS_I18N.onReady(function (i18n) { |
| | | that.currentLocale = i18n.getLocale(); |
| | | that.localeOptions = i18n.getLocaleOptions(); |
| | | that.refreshI18nState(); |
| | | }); |
| | | }, |
| | | refreshI18nState: function () { |
| | | var that = this; |
| | | var homeConfig = this.resolveHomeConfig(); |
| | | var profileConfig = this.resolveProfileConfig(); |
| | | var i; |
| | | HOME_TAB_CONFIG.title = homeConfig.title; |
| | | HOME_TAB_CONFIG.group = homeConfig.group; |
| | | PROFILE_TAB_CONFIG.title = profileConfig.title; |
| | | PROFILE_TAB_CONFIG.group = profileConfig.group; |
| | | for (i = 0; i < this.tabs.length; i++) { |
| | | if (this.isHomeTabUrl(this.tabs[i].url)) { |
| | | this.tabs[i].title = homeConfig.title; |
| | | this.tabs[i].group = homeConfig.group; |
| | | this.tabs[i].home = true; |
| | | } else if (this.resolveViewSrc(this.tabs[i].url) === this.resolveViewSrc(profileConfig.url)) { |
| | | this.tabs[i].title = profileConfig.title; |
| | | this.tabs[i].group = profileConfig.group; |
| | | } else { |
| | | this.tabs[i].title = this.translateTabTitle(this.tabs[i].title); |
| | | this.tabs[i].group = this.tl(this.tabs[i].group); |
| | | } |
| | | } |
| | | this.updateDocumentTitle(this.activeTabTitle); |
| | | this.persistTabs(); |
| | | this.updateLicenseDialogText(); |
| | | this.$nextTick(function () { |
| | | if (window.WCS_I18N) { |
| | | window.WCS_I18N.apply(document.body); |
| | | } |
| | | that.syncAllFramesI18n(); |
| | | }); |
| | | }, |
| | | handleLocaleChange: function (locale) { |
| | | if (window.WCS_I18N) { |
| | | window.WCS_I18N.setLocale(locale); |
| | | } |
| | | }, |
| | | resolveHomeConfig: function () { |
| | | return { |
| | | title: this.t("index.homeTab"), |
| | | url: HOME_TAB_CONFIG.url, |
| | | home: true, |
| | | group: this.t("index.homeGroup"), |
| | | menuKey: HOME_TAB_CONFIG.menuKey || "" |
| | | }; |
| | | }, |
| | | resolveProfileConfig: function () { |
| | | return { |
| | | title: this.t("common.profile"), |
| | | url: PROFILE_TAB_CONFIG.url, |
| | | home: false, |
| | | group: this.t("index.profileGroup"), |
| | | menuKey: PROFILE_TAB_CONFIG.menuKey || "" |
| | | }; |
| | | }, |
| | | translateTabTitle: function (title) { |
| | | return this.tl(title); |
| | | }, |
| | | updateDocumentTitle: function (title) { |
| | | document.title = title + " - " + this.t("app.title"); |
| | | }, |
| | | resolveMenuIcon: function (code) { |
| | | var iconMap = { |
| | | index: "el-icon-s-home", |
| | |
| | | title: config.title, |
| | | name: config.url, |
| | | url: config.url, |
| | | currentSrc: config.url, |
| | | currentSrc: this.addNonce(config.url), |
| | | home: !!config.home, |
| | | group: config.group || "", |
| | | menuKey: config.menuKey || "", |
| | |
| | | }, |
| | | normalizeStoredTab: function (tab) { |
| | | var created = this.createTab({ |
| | | title: tab.title, |
| | | title: this.translateTabTitle(tab.title), |
| | | url: this.resolveViewSrc(tab.url), |
| | | home: !!tab.home, |
| | | group: tab.group || "", |
| | | group: this.tl(tab.group || ""), |
| | | menuKey: tab.menuKey || "" |
| | | }); |
| | | created.loaded = false; |
| | | return created; |
| | | }, |
| | | restoreTabs: function () { |
| | | var homeTab = this.createTab(HOME_TAB_CONFIG); |
| | | var homeTab = this.createTab(this.resolveHomeConfig()); |
| | | var raw = localStorage.getItem(TAB_STORAGE_KEY); |
| | | var parsed; |
| | | var tabs = []; |
| | |
| | | |
| | | this.tabs = tabs; |
| | | this.activeTab = this.hasTab(active) ? active : homeTab.name; |
| | | this.loadingText = "正在加载 “" + this.activeTabTitle + "” ..."; |
| | | this.loadingText = this.t("common.loadingTab", [this.activeTabTitle]); |
| | | this.pageLoading = true; |
| | | document.title = this.activeTabTitle + " - 浙江中扬 - 自动化立体仓库 - WCS"; |
| | | this.updateDocumentTitle(this.activeTabTitle); |
| | | }, |
| | | persistTabs: function () { |
| | | var tabs = []; |
| | |
| | | return !!this.getTabByName(name); |
| | | }, |
| | | isHomeTabUrl: function (url) { |
| | | return this.resolveViewSrc(url || HOME_TAB_CONFIG.url) === this.resolveViewSrc(HOME_TAB_CONFIG.url); |
| | | var homeUrl = this.resolveHomeConfig().url; |
| | | return this.resolveViewSrc(url || homeUrl) === this.resolveViewSrc(homeUrl); |
| | | }, |
| | | getTabByName: function (name) { |
| | | var i; |
| | |
| | | } |
| | | } |
| | | |
| | | this.loadingText = "正在加载 “" + tab.title + "” ..."; |
| | | this.loadingText = this.t("common.loadingTab", [tab.title]); |
| | | this.pageLoading = !tab.loaded; |
| | | this.activeTab = tab.name; |
| | | this.syncMenuStateByUrl(tab.url); |
| | | }, |
| | | openHomeTab: function () { |
| | | this.addOrActivateTab(HOME_TAB_CONFIG); |
| | | this.addOrActivateTab(this.resolveHomeConfig()); |
| | | }, |
| | | openProfileTab: function () { |
| | | this.addOrActivateTab(PROFILE_TAB_CONFIG); |
| | | this.addOrActivateTab(this.resolveProfileConfig()); |
| | | }, |
| | | closeAllTabs: function () { |
| | | this.tabs = [this.createTab(HOME_TAB_CONFIG)]; |
| | | this.activeTab = HOME_TAB_CONFIG.url; |
| | | this.syncMenuStateByUrl(HOME_TAB_CONFIG.url); |
| | | var homeConfig = this.resolveHomeConfig(); |
| | | this.tabs = [this.createTab(homeConfig)]; |
| | | this.activeTab = homeConfig.url; |
| | | this.syncMenuStateByUrl(homeConfig.url); |
| | | this.pageLoading = true; |
| | | this.loadingText = "正在加载 “控制中心” ..."; |
| | | this.loadingText = this.t("common.loadingTab", [homeConfig.title]); |
| | | this.persistTabs(); |
| | | }, |
| | | closeOtherTabs: function () { |
| | | var active = this.getTabByName(this.activeTab); |
| | | var homeTab = this.createTab(HOME_TAB_CONFIG); |
| | | var homeTab = this.createTab(this.resolveHomeConfig()); |
| | | var result = [homeTab]; |
| | | |
| | | if (active && active.name !== homeTab.name) { |
| | |
| | | }, |
| | | removeTab: function (name) { |
| | | var i; |
| | | var nextTabName = HOME_TAB_CONFIG.url; |
| | | var nextTabName = this.resolveHomeConfig().url; |
| | | |
| | | for (i = 0; i < this.tabs.length; i++) { |
| | | if (this.tabs[i].name === name) { |
| | |
| | | } |
| | | |
| | | if (this.tabs.length === 0) { |
| | | this.tabs.push(this.createTab(HOME_TAB_CONFIG)); |
| | | nextTabName = HOME_TAB_CONFIG.url; |
| | | this.tabs.push(this.createTab(this.resolveHomeConfig())); |
| | | nextTabName = this.resolveHomeConfig().url; |
| | | } |
| | | |
| | | this.activeTab = nextTabName; |
| | |
| | | if (tab) { |
| | | tab.loaded = true; |
| | | } |
| | | this.syncFrameI18n(name); |
| | | if (this.activeTab === name) { |
| | | this.pageLoading = false; |
| | | } |
| | |
| | | } |
| | | tab.loaded = false; |
| | | tab.currentSrc = this.addNonce(tab.url); |
| | | this.loadingText = "正在刷新 “" + tab.title + "” ..."; |
| | | this.loadingText = this.t("common.refreshingTab", [tab.title]); |
| | | this.pageLoading = true; |
| | | }, |
| | | addNonce: function (url) { |
| | | return url + (url.indexOf("?") === -1 ? "?" : "&") + "_t=" + new Date().getTime(); |
| | | }, |
| | | findFrameElement: function (name) { |
| | | var frames; |
| | | var i; |
| | | if (!this.$el) { |
| | | return null; |
| | | } |
| | | frames = this.$el.querySelectorAll("iframe[data-tab-name]"); |
| | | for (i = 0; i < frames.length; i++) { |
| | | if (frames[i].getAttribute("data-tab-name") === name) { |
| | | return frames[i]; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | syncFrameI18n: function (name) { |
| | | var that = this; |
| | | var frame = this.findFrameElement(name); |
| | | var frameWindow; |
| | | var frameDocument; |
| | | var script; |
| | | |
| | | function applyFrameI18n() { |
| | | try { |
| | | if (!frameWindow.WCS_I18N || typeof frameWindow.WCS_I18N.onReady !== "function") { |
| | | return; |
| | | } |
| | | frameWindow.WCS_I18N.setLocale(that.currentLocale || "zh-CN", false); |
| | | frameWindow.WCS_I18N.onReady(function (i18n) { |
| | | i18n.apply(frameDocument.body || frameDocument.documentElement); |
| | | if (frameDocument.title) { |
| | | frameDocument.title = i18n.tl(frameDocument.title); |
| | | } |
| | | }); |
| | | } catch (e) { |
| | | } |
| | | } |
| | | |
| | | if (!frame) { |
| | | return; |
| | | } |
| | | try { |
| | | frameWindow = frame.contentWindow; |
| | | frameDocument = frameWindow.document; |
| | | } catch (e) { |
| | | return; |
| | | } |
| | | try { |
| | | frameWindow.localStorage.setItem("wcs_lang", this.currentLocale || "zh-CN"); |
| | | } catch (e) { |
| | | } |
| | | if (frameDocument && frameDocument.documentElement) { |
| | | frameDocument.documentElement.lang = this.currentLocale || "zh-CN"; |
| | | } |
| | | if (frameWindow.WCS_I18N && typeof frameWindow.WCS_I18N.onReady === "function") { |
| | | applyFrameI18n(); |
| | | return; |
| | | } |
| | | if (!frameDocument || !frameDocument.head || frameDocument.getElementById("wcs-i18n-bridge-script")) { |
| | | return; |
| | | } |
| | | script = frameDocument.createElement("script"); |
| | | script.id = "wcs-i18n-bridge-script"; |
| | | script.type = "text/javascript"; |
| | | script.src = baseUrl + "/static/js/common.js"; |
| | | script.onload = applyFrameI18n; |
| | | frameDocument.head.appendChild(script); |
| | | }, |
| | | syncAllFramesI18n: function () { |
| | | var i; |
| | | for (i = 0; i < this.tabs.length; i++) { |
| | | this.syncFrameI18n(this.tabs[i].name); |
| | | } |
| | | }, |
| | | toggleCollapse: function () { |
| | | this.isCollapse = !this.isCollapse; |
| | |
| | | item = group.subMenu[j]; |
| | | subMenu.push({ |
| | | id: item.id, |
| | | name: item.name || item.code || "未命名页面", |
| | | name: item.name || item.code || this.tl("未命名页面"), |
| | | code: item.code || "", |
| | | url: this.buildMenuSrc(item.code, item.id), |
| | | tabKey: this.buildMenuSrc(item.code, item.id) |
| | |
| | | |
| | | result.push({ |
| | | menuId: group.menuId, |
| | | menu: group.menu || "未命名分组", |
| | | menu: group.menu || this.tl("未命名分组"), |
| | | menuCode: group.menuCode || "", |
| | | subMenu: subMenu |
| | | }); |
| | |
| | | }, |
| | | resolveViewSrc: function (path) { |
| | | if (!path) { |
| | | return HOME_TAB_CONFIG.url; |
| | | return this.resolveHomeConfig().url; |
| | | } |
| | | if (/^https?:\/\//.test(path) || path.indexOf(baseUrl) === 0) { |
| | | return path; |
| | |
| | | } |
| | | }, |
| | | syncMenuStateByUrl: function (url) { |
| | | var targetUrl = this.resolveViewSrc(url || HOME_TAB_CONFIG.url); |
| | | var targetUrl = this.resolveViewSrc(url || this.resolveHomeConfig().url); |
| | | var activeMenuKey = ""; |
| | | var groupIndex = ""; |
| | | var that = this; |
| | |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/login"; |
| | | } else { |
| | | that.$message.error(res.msg || "菜单加载失败"); |
| | | that.$message.error(res.msg || that.t("index.menuLoadFailed")); |
| | | } |
| | | }, |
| | | error: function () { |
| | | that.menuLoading = false; |
| | | that.$message.error("菜单加载失败,请检查接口状态"); |
| | | that.$message.error(that.t("index.menuLoadFailedDetail")); |
| | | } |
| | | }); |
| | | }, |
| | |
| | | showPopup: function (days) { |
| | | var currentDate; |
| | | var expiryDate; |
| | | var formattedDate; |
| | | |
| | | if (days === "" || days === null || typeof days === "undefined") { |
| | | this.hidePopup(); |
| | |
| | | currentDate = new Date(); |
| | | expiryDate = new Date(); |
| | | expiryDate.setDate(currentDate.getDate() + Number(days) + 1); |
| | | this.licenseDialogText = "许可证将于 " + new Intl.DateTimeFormat("zh-CN").format(expiryDate) + |
| | | " 过期,剩余有效期:" + days + " 天。"; |
| | | formattedDate = new Intl.DateTimeFormat(this.currentLocale || "zh-CN").format(expiryDate); |
| | | this.licenseDialogText = this.t("index.licenseExpireAt", [formattedDate, days]); |
| | | this.licenseDialogVisible = true; |
| | | }, |
| | | updateLicenseDialogText: function () { |
| | | if (this.licenseDays === "" || this.licenseDays === null || typeof this.licenseDays === "undefined" || this.licenseDays > 15) { |
| | | return; |
| | | } |
| | | this.showPopup(this.licenseDays); |
| | | }, |
| | | hidePopup: function () { |
| | | this.licenseDialogVisible = false; |
| | |
| | | |
| | | if (this.fakeRunning) { |
| | | url = baseUrl + "/openapi/stopFakeSystem"; |
| | | text = "确定要停止仿真模拟吗?"; |
| | | successMsg = "仿真模拟已停止"; |
| | | text = this.t("index.confirmStopFake"); |
| | | successMsg = this.t("index.fakeStoppedSuccess"); |
| | | running = false; |
| | | } else { |
| | | url = baseUrl + "/openapi/startFakeSystem"; |
| | | text = "确定要启动仿真模拟吗?"; |
| | | successMsg = "仿真模拟已启动"; |
| | | text = this.t("index.confirmStartFake"); |
| | | successMsg = this.t("index.fakeStartedSuccess"); |
| | | running = true; |
| | | } |
| | | |
| | | this.$confirm(text, "提示", { |
| | | this.$confirm(text, this.t("common.prompt"), { |
| | | type: "warning" |
| | | }).then(function () { |
| | | $.ajax({ |
| | |
| | | that.fakeRunning = running; |
| | | that.$message.success(successMsg); |
| | | } else { |
| | | that.$message.error(res.msg || "操作失败"); |
| | | that.$message.error(res.msg || that.t("index.operationFailed")); |
| | | } |
| | | } |
| | | }); |
| | |
| | | }, |
| | | showAiTip: function () { |
| | | this.hideAiTip(); |
| | | this.aiTipIndex = layer.tips("AI助手", "#ai-assistant-btn", { |
| | | this.aiTipIndex = layer.tips(this.t("common.aiAssistant"), "#ai-assistant-btn", { |
| | | tips: [1, "#333"], |
| | | time: -1 |
| | | }); |
| | |
| | | startUserSync: function () { |
| | | var that = this; |
| | | this.userSyncTimer = setInterval(function () { |
| | | that.userName = localStorage.getItem(USER_STORAGE_KEY) || "管理员"; |
| | | that.userName = localStorage.getItem(USER_STORAGE_KEY) || that.tl("管理员"); |
| | | }, 1000); |
| | | }, |
| | | installCompatBridge: function () { |
| | |
| | | } |
| | | url = that.resolveViewSrc(param.menuPath); |
| | | that.addOrActivateTab({ |
| | | title: that.stripTags(param.menuName) || "工作页面", |
| | | title: that.stripTags(param.menuName) || that.t("common.workPage"), |
| | | url: url, |
| | | home: false, |
| | | group: "业务页面", |
| | | group: that.t("common.businessPage"), |
| | | menuKey: that.findMenuKeyByUrl(url) |
| | | }); |
| | | }; |
| | |
| | | } |
| | | url = that.resolveViewSrc(param.menuPath); |
| | | that.addOrActivateTab({ |
| | | title: that.stripTags(param.menuName) || HOME_TAB_CONFIG.title, |
| | | title: that.stripTags(param.menuName) || that.resolveHomeConfig().title, |
| | | url: url, |
| | | home: true, |
| | | group: HOME_TAB_CONFIG.group, |
| | | group: that.resolveHomeConfig().group, |
| | | menuKey: that.findMenuKeyByUrl(url) |
| | | }); |
| | | }; |
| | |
| | | border-radius: 4px; |
| | | height: 52px; |
| | | } |
| | | .login-lang { |
| | | position: fixed; |
| | | top: 20px; |
| | | right: 24px; |
| | | z-index: 2; |
| | | } |
| | | .login-lang select { |
| | | min-width: 140px; |
| | | height: 34px; |
| | | padding: 0 10px; |
| | | border: 1px solid #d6dbe6; |
| | | border-radius: 17px; |
| | | background: rgba(255, 255, 255, 0.92); |
| | | color: #3b4a5a; |
| | | outline: none; |
| | | } |
| | | </style> |
| | | </head> |
| | | <body class="login-bg animsition"> |
| | | |
| | | <div class="login-lang"> |
| | | <select id="lang-switch" aria-label="Language"></select> |
| | | </div> |
| | | <div id="login-wrapper" class="animate__animated animate__bounceInDown"> |
| | | <header> |
| | | <h2 id="login-title" style="cursor: pointer; user-select: none;">WCS系统V3.0</h2> |
| | | <h2 id="login-title" data-i18n-key="login.title" style="cursor: pointer; user-select: none;">WCS系统V3.0</h2> |
| | | </header> |
| | | <div class="layui-form layadmin-user-login-body"> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-icon layui-icon-cellphone layadmin-user-login-icon"></label> |
| | | <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" placeholder="账号"> |
| | | <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" data-i18n-placeholder-key="login.username" placeholder="账号"> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-icon layui-icon-password layadmin-user-login-icon"></label> |
| | | <input id="password" class="layui-input" type="password" name="password" lay-verify="password" placeholder="密码"> |
| | | <input id="password" class="layui-input" type="password" name="password" lay-verify="password" data-i18n-placeholder-key="login.password" placeholder="密码"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item login-submit"> |
| | | <button id="login-button" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">登     录</button> |
| | | <button id="login-button" data-i18n-key="login.submit" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">登     录</button> |
| | | </div> |
| | | </div> |
| | | <div id="system-tools-panel" style="display: none; padding: 20px;"> |
| | | <div style="margin-bottom: 18px;"> |
| | | <div style="margin-bottom: 10px; color: #666; font-weight: 600;">推荐操作</div> |
| | | <div data-i18n-key="login.tools.recommended" style="margin-bottom: 10px; color: #666; font-weight: 600;">推荐操作</div> |
| | | <div style="display: flex; flex-wrap: wrap; gap: 12px;"> |
| | | <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">获取请求码</button> |
| | | <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button> |
| | | <button data-i18n-key="login.tools.requestCode" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">获取请求码</button> |
| | | <button data-i18n-key="login.tools.activate" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button> |
| | | </div> |
| | | <div style="margin-top: 8px; color: #999; font-size: 12px;">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div> |
| | | <div data-i18n-key="login.tools.recommendedDesc" style="margin-top: 8px; color: #999; font-size: 12px;">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div> |
| | | </div> |
| | | <div> |
| | | <div style="margin-bottom: 10px; color: #666; font-weight: 600;">其他工具</div> |
| | | <div data-i18n-key="login.tools.others" style="margin-bottom: 10px; color: #666; font-weight: 600;">其他工具</div> |
| | | <div style="display: flex; flex-wrap: wrap; gap: 12px;"> |
| | | <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">获取项目名称</button> |
| | | <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">获取系统配置</button> |
| | | <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">录入许可证</button> |
| | | <button data-i18n-key="login.tools.projectName" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">获取项目名称</button> |
| | | <button data-i18n-key="login.tools.serverInfo" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">获取系统配置</button> |
| | | <button data-i18n-key="login.tools.uploadLicense" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">录入许可证</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | layer = layui.layer, |
| | | $ = layui.jquery; |
| | | |
| | | function initLanguageSwitch() { |
| | | WCS_I18N.onReady(function (i18n) { |
| | | var select = document.getElementById('lang-switch'); |
| | | var options = i18n.getLocaleOptions(); |
| | | var current = i18n.getLocale(); |
| | | var html = ''; |
| | | var i; |
| | | for (i = 0; i < options.length; i++) { |
| | | html += '<option value="' + options[i].tag + '"' + (options[i].tag === current ? ' selected' : '') + '>' + options[i].label + '</option>'; |
| | | } |
| | | select.innerHTML = html; |
| | | select.onchange = function () { |
| | | i18n.setLocale(this.value); |
| | | }; |
| | | document.title = i18n.t('login.title'); |
| | | }); |
| | | } |
| | | |
| | | initLanguageSwitch(); |
| | | |
| | | // 连续点击三次标题显示隐藏功能 |
| | | var titleClickCount = 0; |
| | | var titleClickTimer = null; |
| | |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260307" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/role/role.js" charset="utf-8"></script> |
| | | |
| | |
| | | </body> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260307" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/role/role.js" charset="utf-8"></script> |
| | | </html> |
| | |
| | | </body> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js?v=20260307"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js"></script> |
| | | <script type="text/javascript" src="../../static/js/role/rolePower.js" charset="utf-8"></script> |
| | | </html> |