fix: 修复社区版bug

This commit is contained in:
xieyonghong 2023-04-17 19:01:13 +08:00
parent 159a2f8ca7
commit 126c33ad82
22 changed files with 195 additions and 130 deletions

View File

@ -25,7 +25,7 @@
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"global": "^4.4.0", "global": "^4.4.0",
"jetlinks-store": "^0.0.3", "jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.5", "jetlinks-ui-components": "1.0.5",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"less": "^4.1.3", "less": "^4.1.3",
"less-loader": "^11.1.0", "less-loader": "^11.1.0",

View File

@ -0,0 +1,27 @@
const MaxLengthStringFn = (len: number = 64) => ({
max: len,
message: `最多输入${64}个字符`,
})
export const Max_Length_64 = [MaxLengthStringFn()]
export const Max_Length_200 = [MaxLengthStringFn(200)]
export const RequiredStringFn = (name: string, type: string = 'input') => {
let typeName = '输入'
if (['select', 'date'].includes(type)) {
typeName = '选择'
}
return {
required: true,
message: `${typeName}${name}`,
}
}
export const ID_Rule = [
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
Max_Length_64[0]
]

View File

@ -3,13 +3,5 @@ import { isNoCommunity } from '@/utils/utils';
// 过滤网关类型 // 过滤网关类型
export const accessConfigTypeFilter = (data: any[], filterKey: string = 'id'): any[] => { export const accessConfigTypeFilter = (data: any[], filterKey: string = 'id'): any[] => {
if (!data) return [] if (!data) return []
const filterKeys = !isNoCommunity ? return data.map( item => ({ ...item, label: item.name, value: item.id}))
[
'mqtt-server-gateway',
'http-server-gateway',
'mqtt-client-gateway',
'tcp-server-gateway',
'plugin_gateway'
] : ['plugin_gateway']
return data.filter(item => !filterKeys.includes(item[filterKey])).map( item => ({ ...item, label: item.name, value: item.id}))
} }

View File

@ -53,7 +53,7 @@
</div> </div>
</j-col> </j-col>
</j-row> </j-row>
<j-row :span="24" v-if="AmapKey"> <j-row :span="24" v-if="AmapKey && isNoCommunity">
<j-col :span="24"> <j-col :span="24">
<div class="device-position"> <div class="device-position">
<Guide title="设备分布"></Guide> <Guide title="设备分布"></Guide>
@ -84,6 +84,8 @@ import { useMenuStore } from '@/store/menu';
import Amap from './components/Amap.vue'; import Amap from './components/Amap.vue';
import { useSystem } from '@/store/system'; import { useSystem } from '@/store/system';
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isNoCommunity } from '@/utils/utils'
const system = useSystem(); const system = useSystem();
const AmapKey = system.$state.configInfo.amap?.apiKey; const AmapKey = system.$state.configInfo.amap?.apiKey;
let productTotal = ref(0); let productTotal = ref(0);

View File

@ -4,7 +4,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { ComponentInternalInstance } from 'vue'; import { ComponentInternalInstance, onBeforeUnmount } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@ -46,20 +46,35 @@ const options = computed(() => ({
], ],
})); }));
const myChart = ref()
watch(options, () => { watch(options, () => {
initChart(); if (myChart.value) {
myChart.value.setOption(options.value)
} else {
initChart()
}
}); });
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
})
const resize = () => {
if (myChart.value) {
myChart.value.resize();
}
}
const initChart = () => { const initChart = () => {
nextTick(() => { nextTick(() => {
const myChart = echarts.init(proxy?.$refs[props.chartRef as string] as HTMLElement); myChart.value = echarts.init(proxy?.$refs[props.chartRef as string] as HTMLElement);
myChart.clear(); myChart.value.setOption(options.value);
myChart.setOption(options.value);
window.addEventListener('resize', () => { window.addEventListener('resize', resize);
myChart.resize();
});
}); });
}; };
initChart(); initChart();
</script> </script>

View File

@ -20,15 +20,12 @@ const handle = async (appId: string, url: string) => {
const res = await getAppInfo_api(appId); const res = await getAppInfo_api(appId);
let menuUrl: any = url; let menuUrl: any = url;
if (res.status === 200) { if (res.status === 200) {
// console.log(res.result);
if (res.result.page.routeType === 'hash') { if (res.result.page.routeType === 'hash') {
menuUrl = `/%23/${url}`; menuUrl = `${url}`;
} }
if (res.result.provider === 'internal-standalone') { if (res.result.provider === 'internal-standalone') {
//{baseUrl}/api/application/sso/{appId}/login?redirect={menuUrl}
const urlStandalone = `${res.result.page.baseUrl}/api/application/sso/${appId}/login?redirect=${menuUrl}?layout=false`; const urlStandalone = `${res.result.page.baseUrl}/api/application/sso/${appId}/login?redirect=${menuUrl}?layout=false`;
iframeUrl.value = urlStandalone; iframeUrl.value = urlStandalone;
// console.log(urlStandalone);
} else if (res.result.provider === 'internal-integrated') { } else if (res.result.provider === 'internal-integrated') {
const tokenUrl = `${ const tokenUrl = `${
res.result.page.baseUrl res.result.page.baseUrl
@ -44,7 +41,12 @@ const handle = async (appId: string, url: string) => {
watchEffect(() => { watchEffect(() => {
const params = route.path.split('/')?.[1]; const params = route.path.split('/')?.[1];
const url = route.path.split('/').slice(2).join('/'); const url = route.path.split('/').slice(2).join('/');
console.log(params, url);
handle(params, url); handle(params, url);
}); });
</script> </script>
<style lang='less' scoped>
:deep(.children-full-height) {
margin: 0 !important;
}
</style>

View File

@ -145,12 +145,12 @@ const click = (value: object) => {
display: flex; display: flex;
width: calc(100% - 100px); width: calc(100% - 100px);
.images { .images {
width: 88px; width: 80px;
height: 88px; height: 80px;
img { img {
width: 88px; width: 80px;
height: 88px; height: 80px;
} }
} }

View File

@ -47,6 +47,7 @@ import { serverNode } from '@/api/link/dashboard';
import TopEchartsItemNode from './TopEchartsItemNode.vue'; import TopEchartsItemNode from './TopEchartsItemNode.vue';
import { getWebSocket } from '@/utils/websocket'; import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { isNoCommunity } from '@/utils/utils'
const serverId = ref(); const serverId = ref();
const serverNodeOptions = ref<Array<any>>([]); const serverNodeOptions = ref<Array<any>>([]);
@ -103,17 +104,19 @@ const getData = () => {
}; };
onMounted(() => { onMounted(() => {
serverNode().then((resp: any) => { if (isNoCommunity) {
if (resp.success) { serverNode().then((resp: any) => {
serverNodeOptions.value = resp.result.map((item: any) => ({ if (resp.success) {
label: item.name, serverNodeOptions.value = resp.result.map((item: any) => ({
value: item.id, label: item.name,
})); value: item.id,
if (serverNodeOptions.value.length) { }));
serverId.value = serverNodeOptions.value[0]?.value; if (serverNodeOptions.value.length) {
} serverId.value = serverNodeOptions.value[0]?.value;
} }
}); }
});
}
}); });
watch( watch(
() => serverId.value, () => serverId.value,

View File

@ -30,7 +30,7 @@ import { TOKEN_KEY } from '@/utils/variable';
import { PROTOCOL_UPLOAD } from '@/api/link/protocol'; import { PROTOCOL_UPLOAD } from '@/api/link/protocol';
import { onlyMessage } from '@/utils/comm'; import { onlyMessage } from '@/utils/comm';
import type { UploadChangeParam, UploadProps } from 'ant-design-vue'; import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
import { notification as Notification } from 'ant-design-vue'; import { notification as Notification } from 'jetlinks-ui-components';
import { useSystem } from '@/store/system'; import { useSystem } from '@/store/system';
const emit = defineEmits(['update:modelValue', 'change']); const emit = defineEmits(['update:modelValue', 'change']);

View File

@ -1327,6 +1327,9 @@ const getSupports = async () => {
label: item.name, label: item.name,
value: item.id, value: item.id,
})); }));
if (!typeOptions.value.every((item : any) => item.value === 'UDP')) {
formData.value.type = typeOptions.value[0].value
}
} }
}; };

View File

@ -1,71 +1,73 @@
<template> <template>
<j-spin :spinning='spinning'> <div class='oauth-warp'>
<j-spin :spinning='spinning'>
<div class='oauth'> <div class='oauth' v-if='!spinning'>
<div class='oauth-header'> <div class='oauth-header'>
<div class='oauth-header-left'> <div class='oauth-header-left'>
<img :src='logoImg' alt=''> <img :src='logoImg' alt=''>
</div> </div>
</div>
<div class='oauth-content'>
<!-- 登录 -->
<template v-if='isLogin'>
<div class='oauth-content-header'>
<img :src='headerImg' />
</div>
<div class='oauth-content-content'>
<div class='oauth-content-content-text'>
您正在授权登录,{{ appName }}将获得以下权限:
</div> </div>
<ul>
<li>关联{{userName}}账号</li> <div class='oauth-content'>
<li>获取您的个人信息</li> <!-- 登录 -->
</ul> <template v-if='isLogin'>
<div class='oauth-content-button'> <div class='oauth-content-header'>
<j-button type='primary' @click='() => goOAuth2Fn()'> 同意授权 </j-button> <img :src='headerImg' />
<j-button type='primary' @click='changeAccount'> 切换账号 </j-button> </div>
<div class='oauth-content-content'>
<div class='oauth-content-content-text'>
您正在授权登录,{{ appName }}将获得以下权限:
</div>
<ul>
<li>关联{{userName}}账号</li>
<li>获取您的个人信息</li>
</ul>
<div class='oauth-content-button'>
<j-button type='primary' @click='() => goOAuth2Fn()'> 同意授权 </j-button>
<j-button type='primary' @click='changeAccount'> 切换账号 </j-button>
</div>
</div>
</template>
<template v-else>
<div class='oauth-content-header'>
<img :src='headerImg' />
</div>
<div class='oauth-content-login'>
<j-form layout='horizontal' size='large' :model='formModel' >
<j-form-item name='username'>
<j-input placeholder='用户名' v-model:value='formModel.username' />
</j-form-item>
<j-form-item name='password'>
<j-input-password placeholder='密码' v-model:value='formModel.password' />
</j-form-item>
<j-form-item name='verifyCode' v-if='captcha.base64'>
<j-input placeholder='请输入验证码' v-model:value='formModel.verifyCode' >
<template #addonAfter>
<img
:src='captcha.base64'
@click='getCode'
style='cursor: pointer'
/>
</template>
</j-input>
</j-form-item>
<j-form-item>
<j-button
type='primary'
@click='doLogin'
style='width: 100%'
>
登录
</j-button>
</j-form-item>
</j-form>
</div>
</template>
</div> </div>
</div> </div>
</template> </j-spin>
<template v-else>
<div class='oauth-content-header'> </div>
<img :src='headerImg' />
</div>
<div class='oauth-content-login'>
<j-form layout='horizontal' size='large' :model='formModel' >
<j-form-item name='username'>
<j-input placeholder='用户名' v-model:value='formModel.username' />
</j-form-item>
<j-form-item name='password'>
<j-input-password placeholder='密码' v-model:value='formModel.password' />
</j-form-item>
<j-form-item name='verifyCode' v-if='captcha.base64'>
<j-input placeholder='请输入验证码' v-model:value='formModel.verifyCode' >
<template #addonAfter>
<img
:src='captcha.base64'
@click='getCode'
style='cursor: pointer'
/>
</template>
</j-input>
</j-form-item>
<j-form-item>
<j-button
type='primary'
@click='doLogin'
style='width: 100%'
>
登录
</j-button>
</j-form-item>
</j-form>
</div>
</template>
</div>
</div>
</j-spin>
</template> </template>
<script setup lang='ts' name='Oauth'> <script setup lang='ts' name='Oauth'>
@ -145,15 +147,17 @@ const getLoginUser = async (data?: any) => {
goOAuth2Fn(data) goOAuth2Fn(data)
} }
} else if (res.status === 401) { } else if (res.status === 401) {
isLogin.value = false setTimeout(() => {
spinning.value = false
})
getCode() getCode()
getApplication(data.client_id || params.value.client_id) getApplication(data.client_id || params.value.client_id)
} else { } else {
isLogin.value = false setTimeout(() => {
spinning.value = false
})
} }
setTimeout(() => {
spinning.value = false
})
} }
const getQueryVariable = (variable: any) => { const getQueryVariable = (variable: any) => {
@ -223,6 +227,13 @@ initPage()
</script> </script>
<style scoped lang='less'> <style scoped lang='less'>
.oauth-warp {
height: 500px;
display: flex;
justify-content: center;
align-items: center;
}
.oauth { .oauth {
.oauth-header { .oauth-header {
display: flex; display: flex;

View File

@ -34,7 +34,7 @@
> >
<template #img> <template #img>
<slot name="img"> <slot name="img">
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" /> <img width='80' height='80' :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" />
</slot> </slot>
</template> </template>
<template #content> <template #content>

View File

@ -2,7 +2,7 @@
<div class='device-select'> <div class='device-select'>
<TopCard :options='typeList' v-model:value='selectorModel' @select='select' /> <TopCard :options='typeList' v-model:value='selectorModel' @select='select' />
<DeviceList v-if='selectorModel === "fixed"' :productId='productId' :row-keys='devices' @update='updateDevice' /> <DeviceList v-if='selectorModel === "fixed"' :productId='productId' :row-keys='devices' @update='updateDevice' />
<OrgList v-else-if='selectorModel === "org"' :productId='productId' :row-keys='orgIds' @update='updateOrg' /> <OrgList v-else-if='selectorModel === "org" && isNoCommunity' :productId='productId' :row-keys='orgIds' @update='updateOrg' />
</div> </div>
</template> </template>
@ -13,6 +13,7 @@ import OrgList from './OrgList.vue'
import { getImage } from '@/utils/comm' import { getImage } from '@/utils/comm'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { SelectorValuesItem } from '@/views/rule-engine/Scene/typings' import { SelectorValuesItem } from '@/views/rule-engine/Scene/typings'
import { isNoCommunity } from '@/utils/utils'
type Emit = { type Emit = {
(e: 'update:selector', data: string): void (e: 'update:selector', data: string): void
@ -50,11 +51,14 @@ const selectorModel = ref(props.selector)
const devices = ref(props.deviceKeys) const devices = ref(props.deviceKeys)
const orgIds = ref(props.orgId) const orgIds = ref(props.orgId)
const typeList = [ const typeList = ref([
{ label: '自定义', value: 'fixed', tip: '自定义选择当前产品下的任意设备', img: getImage('/scene/device-custom.png')}, { label: '自定义', value: 'fixed', tip: '自定义选择当前产品下的任意设备', img: getImage('/scene/device-custom.png')},
{ label: '全部', value: 'all', tip: '产品下的所有设备', img: getImage('/scene/trigger-device-all.png')}, { label: '全部', value: 'all', tip: '产品下的所有设备', img: getImage('/scene/trigger-device-all.png')},
{ label: '按组织', value: 'org', tip: '选择产品下归属于具体组织的设备', img: getImage('/scene/trigger-device-org.png')}, ])
]
if (isNoCommunity) {
typeList.value.push({ label: '按组织', value: 'org', tip: '选择产品下归属于具体组织的设备', img: getImage('/scene/trigger-device-org.png')},)
}
const select = (s: string) => { const select = (s: string) => {
selectorModel.value = s selectorModel.value = s

View File

@ -31,7 +31,7 @@
> >
<template #img> <template #img>
<slot name="img"> <slot name="img">
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" /> <img width='80' height='80' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot> </slot>
</template> </template>
<template #content> <template #content>

View File

@ -4,6 +4,7 @@
:columns="columns" :columns="columns"
target="category" target="category"
@search="(params:any)=>queryParams = {...params}" @search="(params:any)=>queryParams = {...params}"
style='margin-bottom: 0;'
/> />
<FullPage> <FullPage>
<j-pro-table <j-pro-table

View File

@ -6,7 +6,7 @@
<LeftTree @change="(id) => (departmentId = id)" /> <LeftTree @change="(id) => (departmentId = id)" />
</div> </div>
<div class="right"> <div class="right">
<j-tabs v-model:activeKey="activeKey" destroyInactiveTabPane> <j-tabs v-if='isNoCommunity' v-model:activeKey="activeKey" destroyInactiveTabPane>
<j-tab-pane key="product" tab="产品"> <j-tab-pane key="product" tab="产品">
<Product <Product
:parentId="departmentId" :parentId="departmentId"
@ -23,6 +23,7 @@
<User :parentId="departmentId" /> <User :parentId="departmentId" />
</j-tab-pane> </j-tab-pane>
</j-tabs> </j-tabs>
<User :parentId="departmentId" v-else />
</div> </div>
</div> </div>
</div> </div>
@ -34,6 +35,7 @@ import LeftTree from './components/LeftTree.vue';
import Product from './product/index.vue'; import Product from './product/index.vue';
import Device from './device/index.vue'; import Device from './device/index.vue';
import User from './user/index.vue'; import User from './user/index.vue';
import { isNoCommunity } from '@/utils/utils'
const activeKey = ref<'product' | 'device' | 'user'>('product'); const activeKey = ref<'product' | 'device' | 'user'>('product');

View File

@ -4,6 +4,7 @@
:columns="columns" :columns="columns"
target="category" target="category"
@search="(params:any)=>queryParams = {...params}" @search="(params:any)=>queryParams = {...params}"
style='margin-bottom: 0;'
/> />
<FullPage> <FullPage>
<j-pro-table <j-pro-table

View File

@ -4,6 +4,7 @@
:columns="columns" :columns="columns"
target="category" target="category"
@search="handleParams" @search="handleParams"
style='margin-bottom: 0;'
/> />
<FullPage> <FullPage>
<j-pro-table <j-pro-table
@ -31,6 +32,7 @@
:hasPermission="`${permission}:bind-user`" :hasPermission="`${permission}:bind-user`"
@click="dialogVisible = true" @click="dialogVisible = true"
style="margin-right: 15px" style="margin-right: 15px"
:disabled='!parentId'
> >
<AIcon type="PlusOutlined" />绑定用户 <AIcon type="PlusOutlined" />绑定用户
</PermissionButton> </PermissionButton>

View File

@ -242,6 +242,7 @@
route.params.id === ':id' ? 'add' : 'update' route.params.id === ':id' ? 'add' : 'update'
}`" }`"
@click="form.clickSave" @click="form.clickSave"
:loading='form.saveLoading'
> >
保存 保存
</PermissionButton> </PermissionButton>

View File

@ -85,11 +85,17 @@ import { message } from 'jetlinks-ui-components';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { MESSAGE_SUBSCRIBE_MENU_CODE, USER_CENTER_MENU_CODE } from '@/utils/consts' import { MESSAGE_SUBSCRIBE_MENU_CODE, USER_CENTER_MENU_CODE } from '@/utils/consts'
const admin = useUserInfo().userInfos?.type.id === 'admin'; import { storeToRefs } from 'pinia';
const permission = 'system/Menu'; const permission = 'system/Menu';
const router = useRouter(); const router = useRouter();
const userInfoStore = useUserInfo()
const { userInfos } = storeToRefs(userInfoStore)
const admin = computed(() => {
return userInfos.value?.username === 'admin';
})
const columns = [ const columns = [
{ {

View File

@ -34,7 +34,7 @@ export default defineConfig(({ mode}) => {
build: { build: {
outDir: 'dist', outDir: 'dist',
assetsDir: 'assets', assetsDir: 'assets',
sourcemap: true, sourcemap: false,
cssCodeSplit: false, cssCodeSplit: false,
manifest: true, manifest: true,
chunkSizeWarningLimit: 2000, chunkSizeWarningLimit: 2000,
@ -92,9 +92,9 @@ export default defineConfig(({ mode}) => {
proxy: { proxy: {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
// target: 'http://192.168.32.226:8844', target: 'http://192.168.32.226:8844',
// target: 'http://192.168.32.244:8881', // target: 'http://192.168.32.244:8881',
target: 'http://120.77.179.54:8844', // 120测试 // target: 'http://120.77.179.54:8844', // 120测试
// target: 'http://192.168.33.46:8844', // 本地开发环境 // target: 'http://192.168.33.46:8844', // 本地开发环境
ws: 'ws://192.168.33.46:8844', ws: 'ws://192.168.33.46:8844',
changeOrigin: true, changeOrigin: true,

View File

@ -3718,10 +3718,10 @@ jetlinks-store@^0.0.3:
resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz" resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz"
integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q== integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q==
jetlinks-ui-components@^1.0.5: jetlinks-ui-components@1.0.5:
version "1.0.5" version "1.0.5"
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#dd86644756d6044c4842193ea72335688cfa77d5" resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#9fa4680c69471ee9abf518782faf1b4c276aa305"
integrity sha512-NFjJRwFuluUEAuFguyLYidgFy3tDuh1lKg10uBR//zFxmIzRaGPNN1r9nt/hp3BIGdQLJHUKgDJXQX6fZmfARg== integrity sha512-pFZ0ol0jjIrrIEqPOFmrS5K623QzczYVMFPf8NQ3XeSBLksW9dncgVEPa6cZZ+9jjwAgWHo2MyPGXZcY6SF8PQ==
dependencies: dependencies:
"@vueuse/core" "^9.12.0" "@vueuse/core" "^9.12.0"
"@vueuse/router" "^9.13.0" "@vueuse/router" "^9.13.0"
@ -6419,13 +6419,6 @@ tough-cookie@~2.5.0:
psl "^1.1.28" psl "^1.1.28"
punycode "^2.1.1" punycode "^2.1.1"
treer@^1.0.4:
version "1.0.4"
resolved "http://registry.jetlinks.cn/treer/-/treer-1.0.4.tgz#3b5ff47baec11c97476b8b7712da71cf459d52a3"
integrity sha512-Q2oAOOxzGYq+/KoKT1bZSSwDL4Y4WYukjiYBAn6GrNlt1xKKf5JS216dsSzkPDoYTHA9CXdbYkehUG7F1JT+5A==
dependencies:
commander "^2.9.0"
treeverse@^1.0.4: treeverse@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "http://47.108.170.157:9013/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" resolved "http://47.108.170.157:9013/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f"