diff --git a/components.d.ts b/components.d.ts index 0af5c053aa2ddd287c35ec14d289b5ece44d71e3..85026829d2c75545647edeae486267ad77cbe5fc 100644 --- a/components.d.ts +++ b/components.d.ts @@ -14,6 +14,7 @@ declare module 'vue' { AppIssue: typeof import('./src/components/AppIssue.vue')['default'] AppPull: typeof import('./src/components/AppPull.vue')['default'] AppVerify: typeof import('./src/components/AppVerify.vue')['default'] + CookieNotice: typeof import('./src/components/CookieNotice.vue')['default'] DocAnchor: typeof import('./src/components/DocAnchor.vue')['default'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] diff --git a/package.json b/package.json index ea45d8796e470f82e4f78553c4d3b424db8db8ad..eabf78a8a2c13a410ac947536f6261e890e0668d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "axios": "^1.6.8", "element-plus": "^2.3.4", "js-base64": "^3.7.5", + "js-cookie": "^3.0.5", "lodash-es": "^4.17.21", "opendesign": "link:opendesign", "pinia": "^2.1.6", @@ -36,6 +37,7 @@ "vue-router": "^4.3.0" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "@types/lodash-es": "^4.17.7", "@types/node": "18.16.19", "@types/prismjs": "^1.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 990be72c9df60230b18fa845338092e60322a29d..27a3e52a7ae7f11297d37e7f2f279865bc75af20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: js-base64: specifier: ^3.7.5 version: 3.7.5 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -53,6 +56,9 @@ importers: specifier: ^4.3.0 version: 4.3.0(vue@3.3.4) devDependencies: + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/lodash-es': specifier: ^4.17.7 version: 4.17.7 @@ -438,6 +444,9 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.11': resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -1743,6 +1752,10 @@ packages: js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3184,6 +3197,8 @@ snapshots: '@types/minimatch': 5.1.2 '@types/node': 18.16.19 + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.11': {} '@types/lodash-es@4.17.7': @@ -4579,6 +4594,8 @@ snapshots: js-base64@3.7.5: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: diff --git a/src/App.vue b/src/App.vue index 2dd05ae9909f68f1f5bb0bbae036373188ecd6df..9711bfe8d34392d018d7c032e22faebd7e8d40cc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,6 +10,7 @@ import AppHeader from '@/components/AppHeader.vue'; import AppFooter from '@/components/AppFooter.vue'; import { refreshInfo } from '@/shared/login'; +import CookieNotice from './components/CookieNotice.vue'; refreshInfo(); const { locale } = useI18n(); @@ -31,6 +32,7 @@ watch( diff --git a/src/assets/svg-icons/icon-close.svg b/src/assets/svg-icons/icon-close.svg new file mode 100644 index 0000000000000000000000000000000000000000..2a02c9a61a54817db345e79a34661a38b967e82b --- /dev/null +++ b/src/assets/svg-icons/icon-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CookieNotice.vue b/src/components/CookieNotice.vue new file mode 100644 index 0000000000000000000000000000000000000000..f1376baec3dde27f776ed6d5d123ccfad0c35b7a --- /dev/null +++ b/src/components/CookieNotice.vue @@ -0,0 +1,394 @@ + + + + + diff --git a/src/components/hooks/useScreen.ts b/src/components/hooks/useScreen.ts new file mode 100644 index 0000000000000000000000000000000000000000..25e579d49a6f92af6db52e67f175b31907bbb724 --- /dev/null +++ b/src/components/hooks/useScreen.ts @@ -0,0 +1,159 @@ +import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'; + +export enum Size { + Phone = 'phone', + PadV = 'pad_v', + PadH = 'pad_h', + Laptop = 'laptop', +} + +export type ScreenSizeT = + | typeof Size.Phone + | Size.PadV + | Size.PadH + | Size.Laptop; + +export const ScreenConfig = { + [Size.Phone]: 600, + [Size.PadV]: 840, + [Size.PadH]: 1200, + [Size.Laptop]: 1440, +}; + +/** + * lt: less than, 小于 < + * le: less than or equal to, 小于等于 <= + * eq: equal to, 等于 = + * ne: never equal to, 不等于 != + * ge: greater than or equal to, 大于等于 >= + * gt: greater than, 大于 > + */ +export type CompareT = 'lt' | 'le' | 'eq' | 'ne' | 'ge' | 'gt'; + +const CompareHandler = { + lt: (a: number, b: number) => a < b, + le: (a: number, b: number) => a <= b, + eq: (a: number, b: number) => a === b, + ne: (a: number, b: number) => a !== b, + ge: (a: number, b: number) => a >= b, + gt: (a: number, b: number) => a > b, +}; + +export const useScreen = () => { + const screenSize = reactive({ + width: 1440, + height: 0, + }); + + const current = ref(Size.Laptop); + + const getSize = (width = screenSize.width) => { + if (width < ScreenConfig[Size.Phone]) { + return Size.Phone; + } else if (width < ScreenConfig[Size.PadV]) { + return Size.PadV; + } else if (width < ScreenConfig[Size.PadH]) { + return Size.PadH; + } else { + return Size.Laptop; + } + }; + + const compare = (type: CompareT = 'eq', size: ScreenSizeT) => { + const w1 = screenSize.width; + const w2 = ScreenConfig[size]; + const handler = CompareHandler[type]; + return handler(w1, w2); + }; + + /** + * phone + */ + const isPhone = computed(() => compare('le', Size.Phone)); // [0, 600] + const gtPhone = computed(() => compare('gt', Size.Phone)); // [601, -] + + /** + * pad + */ + const isPad = computed( + () => compare('gt', Size.Phone) && compare('le', Size.PadH) + ); // [601, 1200] + const lePad = computed(() => compare('le', Size.PadH)); // [0, 1200] + const gtPad = computed(() => compare('gt', Size.PadH)); // [1201, -] + + /** + * pad_v + */ + const isPadV = computed( + () => compare('gt', Size.Phone) && compare('le', Size.PadV) + ); // [601, 840] + const lePadV = computed(() => compare('le', Size.PadV)); // [0, 840] + const gtPadV = computed(() => compare('gt', Size.PadV)); // [841, -] + + /** + * pad_h + */ + const isPadH = computed( + () => compare('gt', Size.PadV) && compare('le', Size.PadH) + ); // [841, 1200] + + /** + * laptop + */ + const isLaptop = computed( + () => compare('gt', Size.PadH) && compare('le', Size.Laptop) + ); // [1201, 1440] + const leLaptop = computed(() => compare('le', Size.Laptop)); // [0, 1440] + const gtLaptop = computed(() => compare('gt', Size.Laptop)); // [1441, -] + const isPadToLaptop = computed( + () => compare('gt', Size.Phone) && compare('le', Size.Laptop) + ); // [601, 1440] + const isPadVToLaptop = computed( + () => compare('gt', Size.PadV) && compare('le', Size.Laptop) + ); // [841, 1440] + + const onWindowResize = () => { + const { innerWidth, innerHeight } = window; + screenSize.width = innerWidth; + screenSize.height = innerHeight; + current.value = getSize(); + }; + + onMounted(() => { + window.addEventListener('resize', onWindowResize); + onWindowResize(); + nextTick(() => onWindowResize()); + }); + + onUnmounted(() => { + window.removeEventListener('resize', onWindowResize); + }); + + return { + // 获取屏幕宽度分级 + getSize, + // 当前屏幕分级 + current, + // 当前屏幕宽度 + size: screenSize, + + isPhone, // [0, 600] + gtPhone, // [601, -] + + isPad, // [601, 1200] + lePad, // [0, 1200] + gtPad, // [1201, -] + + isPadV, // [601, 840] + lePadV, // [0, 840] + gtPadV, // [841, -] + + isPadH, // [841, 1200] + + isLaptop, // [1201, 1440] + leLaptop, // [0, 1440] + gtLaptop, // [1441, -] + isPadToLaptop, // [601, 1440] + isPadVToLaptop, // [841, 1440] + }; +}; diff --git a/src/i18n/cookie/cookie-en.ts b/src/i18n/cookie/cookie-en.ts new file mode 100644 index 0000000000000000000000000000000000000000..716aceb422cc1276c18c942ab5d1456c9896f8e5 --- /dev/null +++ b/src/i18n/cookie/cookie-en.ts @@ -0,0 +1,17 @@ +export default { + title: 'openEuler Community Respects Your Privacy.', + desc: 'his site uses cookies from us and our partners to improve your browsing experience and make the site work properly. By clicking "Accept All", you consent to the use of cookies. By clicking "Reject All", you disable the use of unnecessary cookies. You can manage your cookie settings by clicking "Manage Cookies". For more information or to change your cookie settings, please refer to our ', + link: 'About Cookies', + acceptAll: 'Accept All', + rejectAll: 'Reject All', + manage: 'Manage Cookies', + necessaryCookie: 'Strictly Necessary Cookies', + necessaryCookieTip: 'Always active', + necessaryCookieDetail: + 'These cookies are necessary for the site to work properly and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as logging in or filling in forms. You can set the browser to block these cookies, but that can make parts of the site not work. These cookies do not store any personally identifiable information.', + analyticalCookie: 'Analytics Cookies', + analyticalCookieDetail: + 'We will use these cookies only with your consent. These cookies help us make improvements by collecting statistics such as the number of visits and traffic sources.', + saveSetting: 'Save and Accept', + allowAll: 'Accept All', +}; diff --git a/src/i18n/cookie/cookie-zh.ts b/src/i18n/cookie/cookie-zh.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9d1c58582e7ee0a242465873b0e406e07b608bc --- /dev/null +++ b/src/i18n/cookie/cookie-zh.ts @@ -0,0 +1,17 @@ +export default { + title: 'openEuler社区重视您的隐私', + desc: '我们在本网站上使用Cookie,包括第三方Cookie,以便网站正常运行和提升浏览体验。单击“全部接受”即表示您同意这些目的;单击“全部拒绝”即表示您拒绝非必要的Cookie;单击“管理Cookie”以选择接受或拒绝某些Cookie。需要了解更多信息或随时更改您的Cookie首选项,请参阅我们的', + link: '《关于cookies》', + acceptAll: '全部接受', + rejectAll: '全部拒绝', + manage: '管理Cookie', + necessaryCookie: '必要Cookie', + necessaryCookieTip: '始终启用', + necessaryCookieDetail: + '这些Cookie是网站正常工作所必需的,不能在我们的系统中关闭。它们通常仅是为了响应您的服务请求而设置的,例如登录或填写表单。您可以将浏览器设置为阻止Cookie来拒绝这些Cookie,但网站的某些部分将无法正常工作。这些Cookie不存储任何个人身份信息。', + analyticalCookie: '统计分析Cookie', + analyticalCookieDetail: + '我们将根据您的同意使用和处理这些非必要Cookie。这些Cookie允许我们获得摘要统计数据,例如,统计访问量和访问者来源,便于我们改进我们的网站。', + saveSetting: '保存并接受', + allowAll: '全部接受', +}; diff --git a/src/i18n/cookie/index.ts b/src/i18n/cookie/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a92f0d20f74183595cb1bcdcc6379de062e849bd --- /dev/null +++ b/src/i18n/cookie/index.ts @@ -0,0 +1,7 @@ +import zh from './cookie-zh'; +import en from './cookie-en'; + +export default { + zh, + en, +}; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5262970c5c6a4b7a41b873f7fcd7eab856ff6dbd..3d99b4af9837997e869d4e031e26bf0247d6ba44 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -4,17 +4,20 @@ import sig from './sig'; import common from './common'; import quickIssue from './quick-issue'; +import cookie from './cookie'; const messages = { zh: { common: common.zh, sig: sig.zh, quickIssue: quickIssue.zh, + cookie: cookie.zh, }, en: { common: common.en, sig: sig.en, quickIssue: quickIssue.en, + cookie: cookie.en, }, }; diff --git a/src/main.ts b/src/main.ts index 7911140dc2fa3347f27dd1d24532e2df9fe59d78..927292315e3493b635b2e4e6f225e914cd50b7dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,6 @@ import Prism from 'prismjs'; import App from './App.vue'; import OpenDesign from 'opendesign'; -import { enableOA, reportPerformance } from './shared/analytics'; VueMarkdownEditor.use(vuepressTheme, { Prism, @@ -26,7 +25,4 @@ app.use(OpenDesign); app.use(router); app.use(VueMarkdownEditor); -enableOA(); -reportPerformance(); - app.mount('#app'); diff --git a/src/routers/index.ts b/src/routers/index.ts index 5c26d576b1ad18bd32d1b71c8f65a05ee029d168..d1c09e3b2014bcc997e2c39aada6fd5f3843f924 100644 --- a/src/routers/index.ts +++ b/src/routers/index.ts @@ -1,6 +1,5 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { useLangStore } from '@/stores'; -import { reportPV } from '@/shared/analytics'; export const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/zh/issues' }, @@ -34,20 +33,9 @@ export const router = createRouter({ }, }); -router.beforeEach((to, from) => { +router.beforeEach((to) => { // 设置语言 const langStore = useLangStore(); const lang = to.fullPath.includes('en') ? 'en' : 'zh'; langStore.lang = lang; - - if (from.path === '/') { - return; - } - to.meta.$referrer = window.location.href; -}); - -router.afterEach((to, from) => { - if (to.path !== from.path) { - reportPV(to.meta.$referrer as string); - } }); diff --git a/src/shared/analytics.ts b/src/shared/analytics.ts index 62b553f314a2d45686db936c85e20a878c90a7fb..e7ee56699d8b0d3de413ff3bcf59654d8c90212e 100644 --- a/src/shared/analytics.ts +++ b/src/shared/analytics.ts @@ -4,21 +4,62 @@ import { getClientInfo, } from '@opensig/open-analytics'; import { reportAnalytics } from '@/api/api-analytics'; +import { Router } from 'vue-router'; +import { COOKIE_AGREED_STATUS, useCookieStore } from '@/stores/cookies'; export const oa = new OpenAnalytics({ appKey: 'openEuler', request: (data) => { + if ( + useCookieStore().getUserCookieStatus() !== COOKIE_AGREED_STATUS.ALL_AGREED + ) { + disableOA(); + return; + } reportAnalytics(data); }, }); -export const enableOA = () => { +let routerGuards: (() => void)[]; + +export const enableOA = async (router: Router) => { + if (oa.enabled) { + return; + } oa.setHeader(getClientInfo()); oa.enableReporting(true); + reportPerformance(); + await router.isReady(); + reportPV(); + (routerGuards ??= []).push( + router.beforeEach((to, from) => { + if (from.path === '/' || to.path === from.path) { + return; + } + to.meta.$referrer = window.location.href; + }), + router.afterEach((to, from) => { + if (to.path === from.path) { + return; + } + reportPV(to.meta.$referrer as string); + }) + ); }; export const disableOA = () => { oa.enableReporting(false); + if (routerGuards) { + routerGuards.forEach((item) => item()); + routerGuards = []; + } + [ + 'oa-openEuler-client', + 'oa-openEuler-events', + 'oa-openEuler-session', + ].forEach((key) => { + localStorage.removeItem(key); + }); }; export const reportPV = ($referrer?: string) => { @@ -40,6 +81,9 @@ export const oaReport = >( eventOptions?: any; } ) => { + if (!oa.enabled) { + return; + } return oa.report( event, async (...opt) => ({ diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 1713facb1720aaaa89e0878b547c66ac6e6162b8..6bd6ca9b1d32acfb20cac51afc990c952da34828 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,3 +1,5 @@ +import Cookies from 'js-cookie'; + // TS 对象key合法检查 export function isValidKey( key: string | number | symbol, @@ -27,3 +29,35 @@ export function getYearByOffset(offset = 8) { return utcTime.getFullYear(); } getYearByOffset(); + +/** + * 获取指定key的cookie值 + * @param key + * @returns 值 + */ +export function getCookie(key: string) { + return Cookies.get(key); +} + +/** + * 设置cookie + * @param key cookie的key + * @param value cookie的值 + * @param day cookie的过期时间 默认1天 + */ +export function setCookie( + key: string, + value: string, + day = 1, + domain: string = location.hostname +) { + Cookies.set(key, value, { expires: day, domain }); +} + +/** + * 删除cookie + * @param key cookie的key + */ +export function removeCookie(key: string) { + Cookies.remove(key); +} diff --git a/src/stores/cookies.ts b/src/stores/cookies.ts new file mode 100644 index 0000000000000000000000000000000000000000..664872ffc257a690e2aaeb78bd5648a588800e4d --- /dev/null +++ b/src/stores/cookies.ts @@ -0,0 +1,44 @@ +import { getCookie } from '@/shared/utils'; +import { defineStore } from 'pinia'; + +export const COOKIE_AGREED_STATUS = { + NOT_SIGNED: '0', // 未签署 + ALL_AGREED: '1', // 同意所有cookie + NECCESSARY_AGREED: '2', // 仅同意必要cookie +}; + +export const COOKIE_KEY = 'agreed-cookiepolicy'; + +export const useCookieStore = defineStore('cookie', { + state: () => ({ + status: COOKIE_AGREED_STATUS.NOT_SIGNED, + version: '20240830', + }), + getters: { + isAllAgreed(state) { + return state.status === COOKIE_AGREED_STATUS.ALL_AGREED; + }, + }, + actions: { + getUserCookieStatus() { + const cookieVal = getCookie(COOKIE_KEY) ?? '0'; + const cookieStatusVal = cookieVal[0]; + const privacyVersionVal = cookieVal.slice(1); + if (privacyVersionVal !== this.version) { + this.status = COOKIE_AGREED_STATUS.NOT_SIGNED; + return COOKIE_AGREED_STATUS.NOT_SIGNED; + } + + if (cookieStatusVal === COOKIE_AGREED_STATUS.ALL_AGREED) { + this.status = COOKIE_AGREED_STATUS.ALL_AGREED; + return COOKIE_AGREED_STATUS.ALL_AGREED; + } else if (cookieStatusVal === COOKIE_AGREED_STATUS.NECCESSARY_AGREED) { + this.status = COOKIE_AGREED_STATUS.NECCESSARY_AGREED; + return COOKIE_AGREED_STATUS.NECCESSARY_AGREED; + } else { + this.status = COOKIE_AGREED_STATUS.NOT_SIGNED; + return COOKIE_AGREED_STATUS.NOT_SIGNED; + } + }, + }, +});